From f33515e17c4b609c6c26e6a6cc15d20cecc52c98 Mon Sep 17 00:00:00 2001 From: vorporeal Date: Mon, 25 May 2026 17:29:34 +0000 Subject: [PATCH 1/2] Fix remote-control session end removing local input box (APP-4604) After QUALITY-726, a `/remote-control` share of a local conversation is a `User` share that carries a sidecar orchestrator `source_task_id`. When that share ended (for example when the session hit the 100MB sharing limit), `cloud_conversation_continuation_ui_state` resolved a task and ran the cloud-handoff continuation logic, which could insert a "conversation ended" tombstone and remove the input box for a still-running local conversation. Gate the continuation logic so it only applies to genuine cloud (ambient agent) sessions: bail out when the session source is set to a non-ambient kind. Sessions with no source (restored cloud-mode panes, finished viewers) are unaffected. Co-Authored-By: Oz --- .../terminal/view/shared_session/view_impl.rs | 26 ++++-- .../view/shared_session/view_impl_tests.rs | 89 ++++++++++++++++--- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/app/src/terminal/view/shared_session/view_impl.rs b/app/src/terminal/view/shared_session/view_impl.rs index 47bee75f50..005dc34747 100644 --- a/app/src/terminal/view/shared_session/view_impl.rs +++ b/app/src/terminal/view/shared_session/view_impl.rs @@ -12,10 +12,10 @@ use settings::Setting as _; use warp_core::features::FeatureFlag; use warp_core::semantic_selection::SemanticSelection; use warp_core::ui::appearance::Appearance; +use warpui::r#async::Timer; use warpui::clipboard::ClipboardContent; use warpui::elements::MouseStateHandle; use warpui::platform::Cursor; -use warpui::r#async::Timer; use warpui::ui_components::button::ButtonVariant; use warpui::ui_components::components::UiComponent; use warpui::units::IntoLines; @@ -23,11 +23,11 @@ use warpui::{AppContext, Element, ModelHandle, SingletonEntity, ViewContext}; use super::adapter::{Adapter, Kind, Participant}; use super::cloud_conversation_continuation::{ - conversation_failed_before_task_creation, resolve_cloud_conversation_continuation_ui_state, - CloudConversationContinuationUiState, TombstoneCta, + CloudConversationContinuationUiState, TombstoneCta, conversation_failed_before_task_creation, + resolve_cloud_conversation_continuation_ui_state, }; -use super::sharer::inactivity_modal::InactivityModalEvent; use super::sharer::Sharer; +use super::sharer::inactivity_modal::InactivityModalEvent; use super::viewer::Viewer; use super::{ConversationEndedTombstoneEvent, ConversationEndedTombstoneView}; use crate::ai::agent_conversations_model::AgentConversationsModel; @@ -40,6 +40,7 @@ use crate::editor::{InteractionState, ReplicaId}; use crate::menu::{Event as MenuEvent, MenuItem, MenuItemFields}; use crate::server::telemetry::SharingDialogSource; use crate::settings::InputModeSettings; +use crate::terminal::TerminalModel; use crate::terminal::block_list_viewport::ScrollPositionUpdate; use crate::terminal::model::blocks::BlockListPoint; use crate::terminal::model::index::Point; @@ -57,17 +58,16 @@ use crate::terminal::shared_session::role_change_modal::{ }; use crate::terminal::shared_session::settings::SharedSessionSettings; use crate::terminal::shared_session::{ - join_link, SharedSessionActionSource, SharedSessionScrollbackType, SharedSessionSource, - SharedSessionStatus, COPY_LINK_TEXT, + COPY_LINK_TEXT, SharedSessionActionSource, SharedSessionScrollbackType, SharedSessionSource, + SharedSessionStatus, join_link, }; use crate::terminal::view::{ ContextMenuAction, Event, InlineBannerItem, InlineBannerType, PendingUserQueryKind, RichContentInsertionPosition, SharedSessionBanners, SizeUpdateBuilder, TerminalAction, TerminalView, }; -use crate::terminal::TerminalModel; use crate::view_components::{DismissibleToast, ToastFlavor}; -use crate::{send_telemetry_from_ctx, TelemetryEvent}; +use crate::{TelemetryEvent, send_telemetry_from_ctx}; impl TerminalView { pub fn sharer_session_kind(&self) -> Option<&Kind> { @@ -128,6 +128,16 @@ impl TerminalView { { return None; } + // A user-initiated share (e.g. `/remote-control` of a local + // conversation) carries a sidecar orchestrator `source_task_id`, but + // it is not a cloud conversation. The cloud-handoff continuation and + // tombstone UI must only apply to genuine cloud (ambient agent) + // conversations, so bail out for a session whose source is still set + // to a non-ambient kind. Sessions with no source (e.g. restored + // cloud-mode panes or finished viewers) fall through unchanged. + if model.shared_session_source().is_some() && !model.is_shared_ambient_agent_session() { + return None; + } self.ambient_agent_task_id_for_details_panel_from_model(&model, ctx) }; let Some(task_id) = task_id else { diff --git a/app/src/terminal/view/shared_session/view_impl_tests.rs b/app/src/terminal/view/shared_session/view_impl_tests.rs index 23b45d65e2..cdb1499ebc 100644 --- a/app/src/terminal/view/shared_session/view_impl_tests.rs +++ b/app/src/terminal/view/shared_session/view_impl_tests.rs @@ -9,11 +9,11 @@ use warpui::platform::WindowStyle; use warpui::{App, EntityId, TypedActionView, ViewHandle}; use super::*; +use crate::ai::agent::AIAgentInput; use crate::ai::agent::api::ServerConversationToken; use crate::ai::agent::conversation::{ AIAgentHarness, AIConversation, ConversationStatus, ServerAIConversationMetadata, }; -use crate::ai::agent::AIAgentInput; use crate::ai::agent_conversations_model::{ AgentConversationsModel, AgentConversationsModelEvent, AgentRunDisplayStatus, }; @@ -27,17 +27,17 @@ use crate::cloud_object::{Owner, Revision, ServerMetadata, ServerPermissions}; use crate::context_chips::prompt_type::PromptType; use crate::editor::InteractionState; use crate::server::ids::ServerId; -use crate::terminal::model::blocks::{ToTotalIndex as _, INLINE_BANNER_HEIGHT}; +use crate::terminal::TerminalView; +use crate::terminal::model::blocks::{INLINE_BANNER_HEIGHT, ToTotalIndex as _}; +use crate::terminal::view::TerminalAction; #[cfg(all(feature = "local_fs", not(target_family = "wasm")))] use crate::terminal::view::ambient_agent::{ HandoffSubmissionState, PendingHandoff, SnapshotUploadStatus, }; use crate::terminal::view::shared_session::test_utils::terminal_view_for_viewer; -use crate::terminal::view::TerminalAction; -use crate::terminal::TerminalView; use crate::test_util::add_window_with_terminal; use crate::test_util::terminal::initialize_app_for_terminal_view; -use crate::{assert_lines_approx_eq, FeatureFlag}; +use crate::{FeatureFlag, assert_lines_approx_eq}; #[test] fn test_prompt_context_menu_items_shared_session_viewer_no_edit_prompt() { @@ -82,8 +82,8 @@ fn test_prompt_context_menu_items_shared_session_viewer_no_edit_prompt() { } #[test] -fn test_on_ambient_agent_execution_ended_enables_followup_input_for_editable_non_owner_finished_view( -) { +fn test_on_ambient_agent_execution_ended_enables_followup_input_for_editable_non_owner_finished_view() + { let _handoff_flag = FeatureFlag::HandoffCloudCloud.override_enabled(true); let _setup_v2_flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); @@ -477,8 +477,8 @@ fn test_on_session_share_ended_restores_size_after_viewer_driven_resize() { } #[test] -fn test_on_session_share_ended_does_not_insert_tombstone_for_ambient_session_under_cloud_mode_setup_v2( -) { +fn test_on_session_share_ended_does_not_insert_tombstone_for_ambient_session_under_cloud_mode_setup_v2() + { let _flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); App::test((), |mut app| async move { @@ -1267,8 +1267,8 @@ fn test_on_session_share_ended_clears_frozen_followup_input_for_owned_ambient_se } #[test] -fn test_on_session_share_ended_does_not_insert_tombstone_for_non_ambient_session_under_cloud_mode_setup_v2( -) { +fn test_on_session_share_ended_does_not_insert_tombstone_for_non_ambient_session_under_cloud_mode_setup_v2() + { let _flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); App::test((), |mut app| async move { @@ -1293,6 +1293,73 @@ fn test_on_session_share_ended_does_not_insert_tombstone_for_non_ambient_session }); } +/// Regression test for APP-4604. A `/remote-control` share of a local +/// conversation is a `User` share that, post-QUALITY-726, carries a sidecar +/// orchestrator `source_task_id`. Ending that share (e.g. when the session +/// hits the 100MB limit) must not run the cloud-handoff continuation logic and +/// must not insert a "conversation ended" tombstone that removes the input box, +/// even when the resolved task would otherwise produce a tombstone. +#[test] +fn test_on_session_share_ended_keeps_input_for_user_share_with_source_task_id() { + let _handoff_flag = FeatureFlag::HandoffCloudCloud.override_enabled(true); + let _setup_v2_flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); + + App::test((), |mut app| async move { + let terminal = terminal_view_for_viewer(&mut app); + // A failing task owned by another user would resolve to a no-CTA + // tombstone for a genuine cloud (ambient) share. + let mut task = create_cloud_mode_task_for_user("another-user"); + let task_id = task.task_id; + task.state = AmbientAgentTaskState::Failed; + task.status_message = Some(TaskStatusMessage { + message: "Environment setup failed: Failed to run setup command".to_string(), + error_code: Some(TaskStatusErrorCode::EnvironmentSetupFailed), + }); + + AgentConversationsModel::handle(&app).update(&mut app, |model, _| { + model.insert_task_for_test(task); + }); + let initial_block_height_items = terminal.read(&app, |view, _| { + view.model.lock().block_list().block_heights().items().len() + }); + + terminal.update(&mut app, |view, ctx| { + view.input().update(ctx, |input, ctx| { + input.editor().update(ctx, |editor, ctx| { + editor.set_interaction_state(InteractionState::Editable, ctx); + }); + }); + let mut model = view.model.lock(); + // A `/remote-control` share: `User` source carrying the orchestrator + // task id on the sidecar. + model.set_shared_session_source(SharedSessionSource::user(Some(task_id.to_string()))); + // Mimic the sharer cleanup path, which flips the status to + // `NotShared` before `on_session_share_ended` runs. + model.set_shared_session_status(SharedSessionStatus::NotShared); + drop(model); + view.on_session_share_ended(ctx); + }); + + terminal.read(&app, |view, ctx| { + let final_block_height_items = + view.model.lock().block_list().block_heights().items().len(); + // Only the shared session ended banner is inserted; no tombstone. + assert_eq!(final_block_height_items, initial_block_height_items + 1); + assert!(view.conversation_ended_tombstone_view_id.is_none()); + assert_eq!(view.pending_cloud_followup_task_id, None); + // The local input box stays usable. + assert_eq!( + view.input() + .as_ref(ctx) + .editor() + .as_ref(ctx) + .interaction_state(ctx), + InteractionState::Editable + ); + }); + }); +} + #[test] fn test_on_ambient_agent_execution_ended_inserts_tombstone_when_handoff_enabled() { let _handoff_flag = FeatureFlag::HandoffCloudCloud.override_enabled(true); From 1c45921aeaf79fa2175fceeddbb456c6631c9a7d Mon Sep 17 00:00:00 2001 From: David Stern Date: Mon, 25 May 2026 13:41:55 -0400 Subject: [PATCH 2/2] formatting --- .../terminal/view/shared_session/view_impl.rs | 16 +++++++------- .../view/shared_session/view_impl_tests.rs | 22 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/src/terminal/view/shared_session/view_impl.rs b/app/src/terminal/view/shared_session/view_impl.rs index 005dc34747..cb4b3e8561 100644 --- a/app/src/terminal/view/shared_session/view_impl.rs +++ b/app/src/terminal/view/shared_session/view_impl.rs @@ -12,10 +12,10 @@ use settings::Setting as _; use warp_core::features::FeatureFlag; use warp_core::semantic_selection::SemanticSelection; use warp_core::ui::appearance::Appearance; -use warpui::r#async::Timer; use warpui::clipboard::ClipboardContent; use warpui::elements::MouseStateHandle; use warpui::platform::Cursor; +use warpui::r#async::Timer; use warpui::ui_components::button::ButtonVariant; use warpui::ui_components::components::UiComponent; use warpui::units::IntoLines; @@ -23,11 +23,11 @@ use warpui::{AppContext, Element, ModelHandle, SingletonEntity, ViewContext}; use super::adapter::{Adapter, Kind, Participant}; use super::cloud_conversation_continuation::{ - CloudConversationContinuationUiState, TombstoneCta, conversation_failed_before_task_creation, - resolve_cloud_conversation_continuation_ui_state, + conversation_failed_before_task_creation, resolve_cloud_conversation_continuation_ui_state, + CloudConversationContinuationUiState, TombstoneCta, }; -use super::sharer::Sharer; use super::sharer::inactivity_modal::InactivityModalEvent; +use super::sharer::Sharer; use super::viewer::Viewer; use super::{ConversationEndedTombstoneEvent, ConversationEndedTombstoneView}; use crate::ai::agent_conversations_model::AgentConversationsModel; @@ -40,7 +40,6 @@ use crate::editor::{InteractionState, ReplicaId}; use crate::menu::{Event as MenuEvent, MenuItem, MenuItemFields}; use crate::server::telemetry::SharingDialogSource; use crate::settings::InputModeSettings; -use crate::terminal::TerminalModel; use crate::terminal::block_list_viewport::ScrollPositionUpdate; use crate::terminal::model::blocks::BlockListPoint; use crate::terminal::model::index::Point; @@ -58,16 +57,17 @@ use crate::terminal::shared_session::role_change_modal::{ }; use crate::terminal::shared_session::settings::SharedSessionSettings; use crate::terminal::shared_session::{ - COPY_LINK_TEXT, SharedSessionActionSource, SharedSessionScrollbackType, SharedSessionSource, - SharedSessionStatus, join_link, + join_link, SharedSessionActionSource, SharedSessionScrollbackType, SharedSessionSource, + SharedSessionStatus, COPY_LINK_TEXT, }; use crate::terminal::view::{ ContextMenuAction, Event, InlineBannerItem, InlineBannerType, PendingUserQueryKind, RichContentInsertionPosition, SharedSessionBanners, SizeUpdateBuilder, TerminalAction, TerminalView, }; +use crate::terminal::TerminalModel; use crate::view_components::{DismissibleToast, ToastFlavor}; -use crate::{TelemetryEvent, send_telemetry_from_ctx}; +use crate::{send_telemetry_from_ctx, TelemetryEvent}; impl TerminalView { pub fn sharer_session_kind(&self) -> Option<&Kind> { diff --git a/app/src/terminal/view/shared_session/view_impl_tests.rs b/app/src/terminal/view/shared_session/view_impl_tests.rs index cdb1499ebc..4949551ae4 100644 --- a/app/src/terminal/view/shared_session/view_impl_tests.rs +++ b/app/src/terminal/view/shared_session/view_impl_tests.rs @@ -9,11 +9,11 @@ use warpui::platform::WindowStyle; use warpui::{App, EntityId, TypedActionView, ViewHandle}; use super::*; -use crate::ai::agent::AIAgentInput; use crate::ai::agent::api::ServerConversationToken; use crate::ai::agent::conversation::{ AIAgentHarness, AIConversation, ConversationStatus, ServerAIConversationMetadata, }; +use crate::ai::agent::AIAgentInput; use crate::ai::agent_conversations_model::{ AgentConversationsModel, AgentConversationsModelEvent, AgentRunDisplayStatus, }; @@ -27,17 +27,17 @@ use crate::cloud_object::{Owner, Revision, ServerMetadata, ServerPermissions}; use crate::context_chips::prompt_type::PromptType; use crate::editor::InteractionState; use crate::server::ids::ServerId; -use crate::terminal::TerminalView; -use crate::terminal::model::blocks::{INLINE_BANNER_HEIGHT, ToTotalIndex as _}; -use crate::terminal::view::TerminalAction; +use crate::terminal::model::blocks::{ToTotalIndex as _, INLINE_BANNER_HEIGHT}; #[cfg(all(feature = "local_fs", not(target_family = "wasm")))] use crate::terminal::view::ambient_agent::{ HandoffSubmissionState, PendingHandoff, SnapshotUploadStatus, }; use crate::terminal::view::shared_session::test_utils::terminal_view_for_viewer; +use crate::terminal::view::TerminalAction; +use crate::terminal::TerminalView; use crate::test_util::add_window_with_terminal; use crate::test_util::terminal::initialize_app_for_terminal_view; -use crate::{FeatureFlag, assert_lines_approx_eq}; +use crate::{assert_lines_approx_eq, FeatureFlag}; #[test] fn test_prompt_context_menu_items_shared_session_viewer_no_edit_prompt() { @@ -82,8 +82,8 @@ fn test_prompt_context_menu_items_shared_session_viewer_no_edit_prompt() { } #[test] -fn test_on_ambient_agent_execution_ended_enables_followup_input_for_editable_non_owner_finished_view() - { +fn test_on_ambient_agent_execution_ended_enables_followup_input_for_editable_non_owner_finished_view( +) { let _handoff_flag = FeatureFlag::HandoffCloudCloud.override_enabled(true); let _setup_v2_flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); @@ -477,8 +477,8 @@ fn test_on_session_share_ended_restores_size_after_viewer_driven_resize() { } #[test] -fn test_on_session_share_ended_does_not_insert_tombstone_for_ambient_session_under_cloud_mode_setup_v2() - { +fn test_on_session_share_ended_does_not_insert_tombstone_for_ambient_session_under_cloud_mode_setup_v2( +) { let _flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); App::test((), |mut app| async move { @@ -1267,8 +1267,8 @@ fn test_on_session_share_ended_clears_frozen_followup_input_for_owned_ambient_se } #[test] -fn test_on_session_share_ended_does_not_insert_tombstone_for_non_ambient_session_under_cloud_mode_setup_v2() - { +fn test_on_session_share_ended_does_not_insert_tombstone_for_non_ambient_session_under_cloud_mode_setup_v2( +) { let _flag = FeatureFlag::CloudModeSetupV2.override_enabled(true); App::test((), |mut app| async move {