Skip to content
Merged
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
4 changes: 4 additions & 0 deletions app/src/ai/blocklist/block/pending_user_query_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ impl PendingUserQueryBlock {
self.selected_text.read().clone()
}

pub fn prompt(&self) -> &str {
&self.prompt
}

/// Clears the text selection state and visual selection highlight.
pub fn clear_selection(&mut self, ctx: &mut ViewContext<Self>) {
self.selection_handle.clear();
Expand Down
16 changes: 16 additions & 0 deletions app/src/ai/blocklist/history_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2049,6 +2049,22 @@ impl BlocklistAIHistoryModel {
None
}

pub fn get_server_conversation_metadata_by_server_token(
&self,
server_token: &ServerConversationToken,
) -> Option<&ServerAIConversationMetadata> {
self.find_conversation_id_by_server_token(server_token)
.and_then(|conversation_id| self.get_server_conversation_metadata(&conversation_id))
.or_else(|| {
self.all_conversations_metadata
.values()
.find(|metadata| {
metadata.server_conversation_token.as_ref() == Some(server_token)
})
.and_then(|metadata| metadata.server_conversation_metadata.as_ref())
})
}

/// Finds an AIConversationId by its server conversation token.
///
/// O(1) lookup via `server_token_to_conversation_id`, which is maintained
Expand Down
12 changes: 5 additions & 7 deletions app/src/ai/blocklist/history_model/conversation_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,11 @@ impl BlocklistAIHistoryModel {

if let Some(conversation) = self.conversations_by_id.get_mut(&canonical_conversation_id)
{
if conversation.server_metadata().is_none() {
conversation.set_server_metadata(server_meta.clone());
restored_conversations_updated += 1;
log::debug!(
"Updated server metadata for restored conversation {canonical_conversation_id} with token {server_token_str}"
);
}
conversation.set_server_metadata(server_meta.clone());
restored_conversations_updated += 1;
log::debug!(
"Updated server metadata for restored conversation {canonical_conversation_id} with token {server_token_str}"
);
}

let stale_metadata: Vec<(AIConversationId, AIConversationMetadata)> = self
Expand Down
49 changes: 49 additions & 0 deletions app/src/ai/blocklist/history_model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,55 @@ fn test_merge_cloud_metadata_updates_already_restored_conversations() {
});
}

#[test]
fn test_merge_cloud_metadata_refreshes_stale_restored_conversation_metadata() {
use crate::ai::agent::conversation::AIConversation;

App::test((), |mut app| async move {
let history_model = app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], &[]));
let terminal_view_id = EntityId::new();
let token = "stale-metadata-token";

let mut conversation = AIConversation::new(false, false);
conversation.set_server_conversation_token(token.to_string());
conversation.set_server_metadata(create_server_metadata(
"Stale Conversation",
token,
1.0,
None,
));
let conversation_id = conversation.id();

history_model.update(&mut app, |model, ctx| {
model.restore_conversations(terminal_view_id, vec![conversation], ctx);
});

history_model.update(&mut app, |model, _| {
model.merge_cloud_conversation_metadata(vec![create_server_metadata(
"Refreshed Conversation",
token,
2.0,
None,
)]);
});

history_model.read(&app, |model, _| {
let token = ServerConversationToken::new(token.to_string());
let metadata = model
.get_server_conversation_metadata_by_server_token(&token)
.expect("metadata should be available by server token");
assert_eq!(metadata.title, "Refreshed Conversation");
assert_eq!(metadata.usage.credits_spent, 2.0);

let conversation_metadata = model
.conversation(&conversation_id)
.and_then(|conversation| conversation.server_metadata())
.expect("restored conversation metadata should be refreshed");
assert_eq!(conversation_metadata.title, "Refreshed Conversation");
});
});
}

#[test]
fn test_merge_cloud_metadata_reuses_restored_conversation_id_for_token() {
use crate::ai::agent::conversation::AIConversation;
Expand Down
6 changes: 3 additions & 3 deletions app/src/pane_group/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4277,7 +4277,7 @@ impl PaneGroup {
// Insert the conversation ended tombstone (includes Open in Warp button on WASM).
if terminal_manager.is_some() {
terminal_view.update(ctx, |view, ctx| {
view.insert_conversation_ended_tombstone(ctx);
view.insert_conversation_ended_tombstone_with_resolved_cta(ctx);
});
}

Expand Down Expand Up @@ -5710,7 +5710,7 @@ impl PaneGroup {
shared_session::SharedSessionStatus::FinishedViewer
};
view.model.lock().set_shared_session_status(status);
view.insert_conversation_ended_tombstone(ctx);
view.insert_conversation_ended_tombstone_with_resolved_cta(ctx);
});

ActiveAgentViewsModel::handle(ctx).update(ctx, |active_views, ctx| {
Expand Down Expand Up @@ -6359,7 +6359,7 @@ impl PaneGroup {
let terminal_view = terminal_manager.as_ref(ctx).view();
// Insert the conversation ended tombstone (includes Open in Warp button on WASM)
terminal_view.update(ctx, |view, ctx| {
view.insert_conversation_ended_tombstone(ctx);
view.insert_conversation_ended_tombstone_with_resolved_cta(ctx);
});

BlocklistAIHistoryModel::handle(ctx).update(ctx, |history_model, _ctx| {
Expand Down
6 changes: 6 additions & 0 deletions app/src/terminal/model/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,12 @@ impl BlockList {
self.pinned_to_bottom = Some(view_id);
}

pub(in crate::terminal) fn unpin_rich_content_from_bottom(&mut self, view_id: EntityId) {
if self.pinned_to_bottom == Some(view_id) {
self.pinned_to_bottom = None;
}
}

/// If a rich content item is pinned to the bottom, removes it from its
/// current position and re-appends it so it remains last in the blocklist.
fn maintain_pinned_to_bottom(&mut self) {
Expand Down
83 changes: 59 additions & 24 deletions app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,10 @@ use session_sharing_protocol::common::{
ServerConversationToken as SessionSharingServerConversationToken,
WindowSize as SessionSharingWindowSize,
};
use shared_session::{SharedSessionAdapter, Viewer};
use shared_session::{
cloud_conversation_continuation::CloudConversationContinuationUiState, SharedSessionAdapter,
Viewer,
};
use std::any::Any;
use std::borrow::Cow;
use std::cell::RefCell;
Expand Down Expand Up @@ -5348,14 +5351,19 @@ impl TerminalView {
if self.pending_user_query_kind != Some(PendingUserQueryKind::CloudMode) {
return;
}
let Some(pending_prompt) = self.pending_user_query_prompt(ctx) else {
return;
};

let initial_conversation_query = ai_block_model
.conversation(ctx)
.and_then(|conversation| conversation.initial_user_query());
let has_renderable_user_query = ai_block_model.inputs_to_render(ctx).iter().any(|input| {
input
.display_user_query(initial_conversation_query.as_ref())
.is_some()
input.user_query().as_deref() == Some(pending_prompt)
|| input
.display_user_query(initial_conversation_query.as_ref())
.as_deref()
== Some(pending_prompt)
});
if has_renderable_user_query {
self.remove_pending_user_query_block(ctx);
Expand Down Expand Up @@ -5441,6 +5449,13 @@ impl TerminalView {
{
self.fetch_and_update_conversation_details_panel(ctx);
}
if matches!(
event,
BlocklistAIHistoryEvent::UpdatedConversationMetadata { .. }
| BlocklistAIHistoryEvent::ConversationServerTokenAssigned { .. }
) {
self.maybe_insert_tombstone_for_non_running_shared_ambient_task(ctx);
}
match event {
BlocklistAIHistoryEvent::AppendedExchange {
exchange_id,
Expand Down Expand Up @@ -5798,7 +5813,7 @@ impl TerminalView {
if !conversation.status().is_in_progress()
&& conversation_output_status_from_conversation(conversation).is_some()
{
self.insert_conversation_ended_tombstone(ctx);
self.insert_conversation_ended_tombstone_with_cta(None, ctx);
}
}
}
Expand Down Expand Up @@ -7177,17 +7192,27 @@ impl TerminalView {
return;
}

let can_continue_owned_task_in_cloud = self.owned_ambient_agent_task_id(ctx).is_some()
&& FeatureFlag::HandoffCloudCloud.is_enabled();
if !can_continue_owned_task_in_cloud {
self.insert_conversation_ended_tombstone(ctx);
} else if !self
.model
.lock()
.shared_session_status()
.is_sharer_or_viewer()
{
self.enable_owned_cloud_followup_input(task_id, ctx)
if FeatureFlag::HandoffCloudCloud.is_enabled() {
let has_live_shared_session = {
let status = self.model.lock().shared_session_status().clone();
status.is_active_viewer() || status.is_active_sharer()
};
if has_live_shared_session {
return;
}
let Some(state) = self.cloud_conversation_continuation_ui_state(ctx) else {
return;
};
match state {
CloudConversationContinuationUiState::Tombstone { cta } => {
self.insert_conversation_ended_tombstone_with_cta(cta, ctx);
}
CloudConversationContinuationUiState::FollowupInput => {
self.enable_cloud_followup_input(task_id, ctx);
}
}
} else {
self.insert_conversation_ended_tombstone_with_cta(None, ctx);
}
}

Expand Down Expand Up @@ -7341,6 +7366,7 @@ impl TerminalView {
// agent exchange arrives, we hide the interactive input view. A non-interactive footer is
// rendered instead (see `TerminalView::render`).
if !FeatureFlag::CloudModeSetupV2.is_enabled()
&& !FeatureFlag::HandoffCloudCloud.is_enabled()
&& ambient_agent::is_cloud_agent_pre_first_exchange(
self.ambient_agent_view_model.as_ref(),
&self.agent_view_controller,
Expand Down Expand Up @@ -7421,6 +7447,22 @@ impl TerminalView {
true
}

fn should_render_legacy_ambient_agent_loading_footer(
&self,
model: &TerminalModel,
app: &AppContext,
) -> bool {
!model.is_read_only()
&& !FeatureFlag::CloudModeSetupV2.is_enabled()
&& !FeatureFlag::HandoffCloudCloud.is_enabled()
&& ambient_agent::is_cloud_agent_pre_first_exchange(
self.ambient_agent_view_model.as_ref(),
&self.agent_view_controller,
model,
app,
)
}

/// Give the agent control of the active long running command
/// (which was started outside of a conversation).
fn tag_agent_in(&mut self, ctx: &mut ViewContext<Self>) {
Expand Down Expand Up @@ -26213,14 +26255,7 @@ impl View for TerminalView {

if self.is_input_box_visible(&model, app) {
column.add_child(self.render_input());
} else if !model.is_read_only()
&& ambient_agent::is_cloud_agent_pre_first_exchange(
self.ambient_agent_view_model.as_ref(),
&self.agent_view_controller,
&model,
app,
)
{
} else if self.should_render_legacy_ambient_agent_loading_footer(&model, app) {
column.add_child(ambient_agent::render_loading_footer(appearance));
} else if self.show_remote_server_loading_footer(&model, app) {
column.add_child(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ impl View for CloudModeSetupTextBlock {
&self.ambient_agent_view_model,
app,
)
.with_margin_top(8.)
.finish()
}
}
Expand Down
23 changes: 13 additions & 10 deletions app/src/terminal/view/ambient_agent/view_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,21 @@ impl TerminalView {
};

// Tear down the cloud-mode queued-prompt block on terminal / transition
// events that replace it. `Failed`, `NeedsGithubAuth`, and `Cancelled` hand off
// events that replace it. Legacy `Failed`, `NeedsGithubAuth`, and `Cancelled` hand off
// to the existing error / auth / cancelled UI; `HarnessCommandStarted` hands
// off to the live third-party harness CLI block. Idempotent and cheap when no
// block exists.
if matches!(
event,
AmbientAgentViewModelEvent::Failed { .. }
| AmbientAgentViewModelEvent::NeedsGithubAuth
| AmbientAgentViewModelEvent::Cancelled
| AmbientAgentViewModelEvent::HarnessCommandStarted { .. }
| AmbientAgentViewModelEvent::HandoffSnapshotUploadFailed { .. }
) {
let should_remove_pending_user_query = match event {
AmbientAgentViewModelEvent::Failed { .. } => {
!FeatureFlag::CloudModeSetupV2.is_enabled()
}
AmbientAgentViewModelEvent::NeedsGithubAuth
| AmbientAgentViewModelEvent::Cancelled
| AmbientAgentViewModelEvent::HarnessCommandStarted { .. }
| AmbientAgentViewModelEvent::HandoffSnapshotUploadFailed { .. } => true,
_ => false,
};
if should_remove_pending_user_query {
self.remove_pending_user_query_block(ctx);
}

Expand Down Expand Up @@ -232,7 +235,7 @@ impl TerminalView {
);

if FeatureFlag::CloudModeSetupV2.is_enabled() {
self.insert_conversation_ended_tombstone(ctx);
self.insert_conversation_ended_tombstone_with_resolved_cta(ctx);
}

// Refresh the details panel to show failed status
Expand Down
16 changes: 15 additions & 1 deletion app/src/terminal/view/pending_user_query.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use warp_core::features::FeatureFlag;
use warpui::{SingletonEntity, ViewContext};
use warpui::{AppContext, SingletonEntity, ViewContext};

use crate::{
ai::{
Expand All @@ -13,6 +13,20 @@ use crate::{
use super::rich_content::RichContentMetadata;

impl TerminalView {
pub(super) fn pending_user_query_prompt<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a str> {
let view_id = self.pending_user_query_view_id?;
self.rich_content_views
.iter()
.find_map(|rich_content| match rich_content.metadata() {
Some(RichContentMetadata::PendingUserQuery {
pending_user_query_block_handle,
}) if pending_user_query_block_handle.id() == view_id => {
Some(pending_user_query_block_handle.as_ref(ctx).prompt())
}
_ => None,
})
}

pub(super) fn pending_user_query_conversation_id(&self) -> Option<AIConversationId> {
let view_id = self.pending_user_query_view_id?;
self.rich_content_views
Expand Down
Loading
Loading