From 564ea2ae58960a1ef13b6aa85dbe31c88f33917b Mon Sep 17 00:00:00 2001 From: Roland Huang Date: Mon, 4 May 2026 17:57:52 -0500 Subject: [PATCH] APP-3595: Show conversation details panel for local conversations (#9493) ## Description Closes [APP-3595](https://linear.app/warpdotdev/issue/APP-3595). The conversation details side panel was previously only visible for cloud Oz runs (a `TerminalView` had to expose an `AmbientAgentTaskId`). Local Warp Agent conversations had no way to surface the same metadata even though `ConversationDetailsPanel` and `ConversationDetailsData::from_conversation` already supported that case on web. This PR makes the pane-header info button and the side panel available for **any active AI conversation in a `TerminalView`** (cloud Oz runs continue to work unchanged). Task-only sections (Run ID, Environment details, setup commands, error message, inference/compute credit split) stay hidden when there's no backing `AmbientAgentTask`; everything else (status, working directory, conversation ID when persisted, run time, credits, source prompt, artifacts, harness, creator) renders from the active `AIConversation`. ### Behavior summary - **Cloud Oz runs**: unchanged. Info button in pane header, auto-open on `SessionReady`, all existing fields rendered. - **Cloud non-Oz runs (Claude / Gemini)**: unchanged. Task-mode panel as today. - **Local AI conversations (native)**: a new info-button toggle is visible whenever there's an active conversation and AI is enabled. Clicking opens the panel populated from the active `AIConversation`. Task-only sections are hidden because the data isn't available. - The panel does **not** auto-open for local conversations — it only opens when the user clicks the toggle. Auto-open behavior on `SessionReady` is still scoped to cloud mode. - The panel refreshes on `BlocklistAIControllerEvent::FinishedReceivingOutput` for any active AI conversation (previously: only when the view was an ambient agent), and on relevant `BlocklistAIHistoryEvent` updates so streaming/status/artifact changes flow through while the panel is open. ### Key code changes - `app/src/ai/conversation_details_panel.rs`: lifted the `#[cfg(target_family = "wasm")]` gates on `from_conversation`, `CreatorInfo::from_uid_fallback`, and the `UserUid` / `UserProfiles` imports so the helper compiles on native. - `app/src/terminal/view.rs`: added `TerminalView::has_active_local_ai_conversation` and `TerminalView::can_show_conversation_details_ui`, used the broader predicate in the render gate, and broadened the refresh hooks. - `app/src/terminal/view/ambient_agent/view_impl.rs`: generalized `fetch_and_update_cloud_mode_details_panel` to fall back to the active `AIConversation` when there's no task ID. - `app/src/terminal/view/pane_impl.rs`: lifted the toggle button rendering out from under the `FeatureFlag::CloudMode` arm in `render_header_actions`. - `app/src/ai/conversation_details_panel_tests.rs`: added a unit test asserting that `from_conversation` populates the local-conversation fields the details panel renders (working directory, title, source prompt, `LocalConversation` credits, `Conversation` mode). ## Testing Manually tested: https://www.loom.com/share/1ca886583b3340ea94842ce7202a3c57 - Added `test_from_conversation_populates_local_conversation_fields` in `app/src/ai/conversation_details_panel_tests.rs`. All 5 tests in the `conversation_details_panel` module pass. - `cargo check -p warp --lib` succeeds. - `cargo clippy -p warp --lib --tests --no-deps -- -D warnings` succeeds. - `cargo fmt -p warp` applied. - Manual smoke test (recommended for reviewers): start a local Warp Agent conversation in a terminal pane and verify the info button appears in the pane header, opens the side panel populated with the conversation's title, status, initial query, run time, credits, working directory, and conversation ID (when persisted). Compare to a cloud Oz run for parity. ## Server API dependencies This change is client-only and does not depend on any server API changes. ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode ## Changelog Entries for Stable CHANGELOG-IMPROVEMENT: Conversation details side panel is now available for local Warp Agent conversations, not just cloud Oz runs. Click the info button in the pane header to open it for any active AI conversation. _Conversation: https://staging.warp.dev/conversation/c79b0062-c933-4c75-956d-b16f461656a9_ _Run: https://oz.staging.warp.dev/runs/019ddaf6-a62a-7ef3-86f7-b80b8ac8b2c8_ _Plans_: - _[APP-3595: Show conversation details panel for local conversations](https://staging.warp.dev/drive/notebook/kId8vzGLitcHOj5IMwH4R8)_ _This PR was generated with [Oz](https://warp.dev/oz)._ --------- Co-authored-by: Oz --- app/src/ai/conversation_details_panel.rs | 9 +- .../ai/conversation_details_panel_tests.rs | 76 ++++++++++- app/src/terminal/view.rs | 121 +++++++++++------- app/src/terminal/view/action.rs | 4 +- .../terminal/view/ambient_agent/view_impl.rs | 81 +++++++----- app/src/terminal/view/pane_impl.rs | 57 +++++---- .../terminal/view/shared_session/view_impl.rs | 4 +- .../view/shared_session/view_impl_test.rs | 8 +- 8 files changed, 241 insertions(+), 119 deletions(-) diff --git a/app/src/ai/conversation_details_panel.rs b/app/src/ai/conversation_details_panel.rs index b77c6ed9..c928af00 100644 --- a/app/src/ai/conversation_details_panel.rs +++ b/app/src/ai/conversation_details_panel.rs @@ -28,7 +28,6 @@ use warpui::{ }; use crate::ai::agent::api::ServerConversationToken; -#[cfg(target_family = "wasm")] use crate::ai::agent::conversation::AIConversation; use crate::ai::agent::conversation::{AIConversationId, ConversationStatus}; use crate::ai::agent_conversations_model::AgentRunDisplayStatus; @@ -42,7 +41,6 @@ use crate::ai::blocklist::BlocklistAIHistoryModel; use crate::ai::cloud_environments::{AmbientAgentEnvironment, CloudAmbientAgentEnvironment}; use crate::ai::harness_display; use crate::appearance::Appearance; -#[cfg(target_family = "wasm")] use crate::auth::UserUid; use crate::notebooks::NotebookId; use crate::send_telemetry_from_ctx; @@ -64,7 +62,6 @@ use crate::view_components::copyable_text_field::{ }; use crate::view_components::DismissibleToast; use crate::workspace::{ForkedConversationDestination, ToastStack, WorkspaceAction}; -#[cfg(target_family = "wasm")] use crate::workspaces::user_profiles::UserProfiles; const FIELD_SPACING: f32 = 16.0; @@ -161,7 +158,6 @@ impl CreatorInfo { } /// Create a CreatorInfo with just the first character as a fallback. - #[cfg(target_family = "wasm")] pub fn from_uid_fallback(uid: &str) -> Self { let first_char = uid.chars().next().unwrap_or('?').to_uppercase().to_string(); Self::new(first_char, None) @@ -224,7 +220,10 @@ impl ConversationDetailsData { .and_then(|metadata| metadata.initial_working_directory.clone()) }) } - #[cfg(target_family = "wasm")] + + /// Build details data from an in-memory `AIConversation`. Used both by the WASM + /// transcript/shared-session details panel and by the native pane-level details panel + /// when the active conversation is a local (non-cloud) Warp Agent run. pub fn from_conversation(conversation: &AIConversation, app: &AppContext) -> Self { let mut directory = None; let mut conversation_id = None; diff --git a/app/src/ai/conversation_details_panel_tests.rs b/app/src/ai/conversation_details_panel_tests.rs index aecfd76a..1e1c3ee4 100644 --- a/app/src/ai/conversation_details_panel_tests.rs +++ b/app/src/ai/conversation_details_panel_tests.rs @@ -5,14 +5,14 @@ use persistence::model::AgentConversationData; use warp_cli::agent::Harness; use warp_core::features::FeatureFlag; use warp_multi_agent_api as api; -use warpui::{App, EntityId}; +use warpui::{App, EntityId, SingletonEntity}; use crate::ai::agent::conversation::{AIConversation, AIConversationId}; use crate::ai::ambient_agents::task::{AgentConfigSnapshot, HarnessConfig, TaskCreatorInfo}; use crate::ai::ambient_agents::{AmbientAgentTask, AmbientAgentTaskState}; use crate::ai::blocklist::history_model::BlocklistAIHistoryModel; -use super::{ConversationDetailsData, PanelMode}; +use super::{ConversationDetailsData, CreditsInfo, PanelMode}; fn create_test_task(task_id: &str) -> AmbientAgentTask { let now = Utc::now(); @@ -224,6 +224,78 @@ fn test_from_task_resolves_harness() { }); } +#[test] +fn test_from_conversation_populates_local_conversation_fields() { + // Locks in that `ConversationDetailsData::from_conversation` works on native + // and surfaces the conversation-derived fields the conversation details panel + // renders for local Warp Agent runs (APP-3595). + App::test((), |mut app| async move { + let history_model = app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], &[])); + + let conversation_id = AIConversationId::new(); + let directory = "/tmp/local-conversation-directory"; + let conversation = create_restored_conversation( + conversation_id, + "root-task", + directory, + AgentConversationData { + server_conversation_token: None, + conversation_usage_metadata: None, + reverted_action_ids: None, + forked_from_server_conversation_token: None, + artifacts_json: None, + parent_agent_id: None, + agent_name: None, + parent_conversation_id: None, + run_id: None, + autoexecute_override: None, + last_event_sequence: None, + is_remote_child: false, + }, + ); + + history_model.update(&mut app, |model, ctx| { + model.restore_conversations(EntityId::new(), vec![conversation], ctx); + }); + + app.update(|ctx| { + let conversation = BlocklistAIHistoryModel::as_ref(ctx) + .conversation(&conversation_id) + .expect("conversation should be present"); + let data = ConversationDetailsData::from_conversation(conversation, ctx); + + // Mode should be Conversation with the working directory and no server-side + // conversation id (since this conversation was restored without a server token). + match &data.mode { + PanelMode::Conversation { + directory: panel_directory, + server_conversation_id, + ai_conversation_id, + status, + } => { + assert_eq!(panel_directory.as_deref(), Some(directory)); + assert!(server_conversation_id.is_none()); + // `from_conversation` does not have access to the in-memory + // AIConversationId; that field is populated only by the + // management view path (`from_conversation_metadata`). + assert!(ai_conversation_id.is_none()); + assert!(status.is_some()); + } + PanelMode::Task { .. } => { + panic!("expected Conversation mode for a local conversation") + } + } + + assert_eq!(data.title, "test query"); + assert_eq!(data.source_prompt.as_deref(), Some("test query")); + assert!(matches!( + data.credits, + Some(CreditsInfo::LocalConversation(_)) + )); + }); + }); +} + #[test] fn test_from_task_includes_linked_directory_when_server_token_matches() { App::test((), |mut app| async move { diff --git a/app/src/terminal/view.rs b/app/src/terminal/view.rs index b5ab8ebe..4d30c0b2 100644 --- a/app/src/terminal/view.rs +++ b/app/src/terminal/view.rs @@ -2748,18 +2748,21 @@ pub struct TerminalView { ambient_agent_view_model: Option>, pending_cloud_followup_task_id: Option, - /// Cloud mode conversation details panel (side panel showing task metadata). - cloud_mode_details_panel: + /// Conversation details panel (side panel showing conversation/task metadata). + /// Available for cloud Oz runs and for any active local AI conversation. + conversation_details_panel: ViewHandle, - /// Whether the cloud mode details panel is currently open. - is_cloud_mode_details_panel_open: bool, + /// Whether the conversation details panel is currently open. + is_conversation_details_panel_open: bool, /// Whether we've already auto-opened the panel when the agent started running. - /// This prevents re-opening the panel if the user manually closes it. - has_auto_opened_cloud_mode_details_panel: bool, - /// Mouse state handle for the cloud mode details panel toggle button in the pane header. + /// This prevents re-opening the panel if the user manually closes it. Only set + /// by the cloud-mode auto-open path; local conversations require the user to + /// click the pane-header toggle button to open the panel. + has_auto_opened_conversation_details_panel: bool, + /// Mouse state handle for the conversation details panel toggle button in the pane header. /// Only available on non-WASM platforms (WASM uses a per-window button instead). #[cfg(not(target_arch = "wasm32"))] - cloud_mode_details_panel_toggle_mouse_state: warpui::elements::MouseStateHandle, + conversation_details_panel_toggle_mouse_state: warpui::elements::MouseStateHandle, /// Mouse state handle for the ambient agent cancel button in the pane header. ambient_agent_cancel_mouse_state: warpui::elements::MouseStateHandle, @@ -3493,17 +3496,15 @@ impl TerminalView { ctx.subscribe_to_model(&ai_controller, |me, handle, event, ctx| { me.handle_ai_controller_event(handle, event, ctx); - // Refresh cloud mode details panel when agent output completes (may include new artifacts) + // Refresh the conversation details panel when agent output completes + // (may include new artifacts, run time, credits). This applies to both + // cloud-task-backed and local AI conversations as long as the panel is open. if matches!( event, BlocklistAIControllerEvent::FinishedReceivingOutput { .. } - ) && me.is_cloud_mode_details_panel_open - && me - .ambient_agent_view_model - .as_ref() - .is_some_and(|model| model.as_ref(ctx).is_ambient_agent()) + ) && me.is_conversation_details_panel_open { - me.fetch_and_update_cloud_mode_details_panel(ctx); + me.fetch_and_update_conversation_details_panel(ctx); } }); @@ -3528,13 +3529,13 @@ impl TerminalView { ); // Only refresh panel if it's currently open (avoids unnecessary work) if should_refresh_details_panel - && me.is_cloud_mode_details_panel_open + && me.is_conversation_details_panel_open && me .ambient_agent_view_model .as_ref() .is_some_and(|model| model.as_ref(ctx).is_ambient_agent()) { - me.fetch_and_update_cloud_mode_details_panel(ctx); + me.fetch_and_update_conversation_details_panel(ctx); ctx.notify(); } }, @@ -4029,19 +4030,19 @@ impl TerminalView { }) }); - // Cloud mode conversation details panel - let cloud_mode_details_panel = ctx.add_typed_action_view(|ctx| { + // Conversation details panel (cloud Oz runs and any active local AI conversation). + let conversation_details_panel = ctx.add_typed_action_view(|ctx| { crate::ai::conversation_details_panel::ConversationDetailsPanel::new( false, // don't show "Open" button since we're already viewing the conversation 320.0, // initial width ctx, ) }); - ctx.subscribe_to_view(&cloud_mode_details_panel, |me, _, event, ctx| { + ctx.subscribe_to_view(&conversation_details_panel, |me, _, event, ctx| { use crate::ai::conversation_details_panel::ConversationDetailsPanelEvent; match event { ConversationDetailsPanelEvent::Close => { - me.is_cloud_mode_details_panel_open = false; + me.is_conversation_details_panel_open = false; ctx.notify(); } ConversationDetailsPanelEvent::OpenPlanNotebook { notebook_uid } => { @@ -4183,12 +4184,12 @@ impl TerminalView { orchestration_pill_bar, is_using_conversation_for_pane_header_title: false, ambient_agent_view_model, + conversation_details_panel, + is_conversation_details_panel_open: false, + has_auto_opened_conversation_details_panel: false, pending_cloud_followup_task_id: None, - cloud_mode_details_panel, - is_cloud_mode_details_panel_open: false, - has_auto_opened_cloud_mode_details_panel: false, #[cfg(not(target_arch = "wasm32"))] - cloud_mode_details_panel_toggle_mouse_state: Default::default(), + conversation_details_panel_toggle_mouse_state: Default::default(), ambient_agent_cancel_mouse_state: Default::default(), active_init_project_model: None, is_pending_aws_login: false, @@ -5095,6 +5096,25 @@ impl TerminalView { { return; } + // If the conversation details panel is open and showing an active local + // AI conversation in this terminal view, refresh its data when status, + // artifacts, exchanges, or metadata change. Mirrors the WASM transcript + // panel refresh logic in `Workspace::handle_history_model_event` for + // APP-3595. + if self.is_conversation_details_panel_open + && matches!( + event, + BlocklistAIHistoryEvent::UpdatedConversationStatus { .. } + | BlocklistAIHistoryEvent::UpdatedConversationMetadata { .. } + | BlocklistAIHistoryEvent::UpdatedConversationArtifacts { .. } + | BlocklistAIHistoryEvent::UpdatedStreamingExchange { .. } + | BlocklistAIHistoryEvent::AppendedExchange { .. } + | BlocklistAIHistoryEvent::SetActiveConversation { .. } + | BlocklistAIHistoryEvent::RestoredConversations { .. } + ) + { + self.fetch_and_update_conversation_details_panel(ctx); + } match event { BlocklistAIHistoryEvent::AppendedExchange { exchange_id, @@ -6685,14 +6705,26 @@ impl TerminalView { self.ambient_agent_task_id_for_details_panel_from_model(&model, app) } - fn can_show_cloud_mode_details_ui_for_task_id(task_id: Option) -> bool { - FeatureFlag::CloudMode.is_enabled() && task_id.is_some() + /// Whether the conversation details side panel should be available in the + /// pane header / pane layout for this terminal view. + fn can_show_conversation_details_ui_from_model( + &self, + model: &TerminalModel, + app: &AppContext, + ) -> bool { + self.ambient_agent_task_id_for_details_panel_from_model(model, app) + .is_some() + || BlocklistAIHistoryModel::as_ref(app) + .active_conversation(self.view_id) + .is_some_and(|conversation| !conversation.is_empty()) } - fn can_show_cloud_mode_details_ui(&self, app: &AppContext) -> bool { - Self::can_show_cloud_mode_details_ui_for_task_id( - self.ambient_agent_task_id_for_details_panel(app), - ) + /// Convenience wrapper around + /// [`Self::can_show_conversation_details_ui_from_model`] that locks the + /// terminal model. Do not call from contexts that already hold the lock. + fn can_show_conversation_details_ui(&self, app: &AppContext) -> bool { + let model = self.model.lock(); + self.can_show_conversation_details_ui_from_model(&model, app) } fn maybe_insert_tombstone_for_non_running_shared_ambient_task( @@ -24624,7 +24656,7 @@ impl TypedActionView for TerminalView { | ExitAgentView | EnterCloudAgentView | StartNewAgentConversation - | ToggleCloudModeDetailsPanel + | ToggleConversationDetailsPanel | CancelAmbientAgentTask | OpenInlineHistoryMenu | OpenModelSelector @@ -25640,11 +25672,11 @@ impl TypedActionView for TerminalView { AwsCliNotInstalledBanner(action) => { self.handle_aws_cli_not_installed_banner_action(*action, ctx); } - ToggleCloudModeDetailsPanel => { - let will_open = !self.is_cloud_mode_details_panel_open; - self.is_cloud_mode_details_panel_open = will_open; + ToggleConversationDetailsPanel => { + let will_open = !self.is_conversation_details_panel_open; + self.is_conversation_details_panel_open = will_open; if will_open { - self.fetch_and_update_cloud_mode_details_panel(ctx); + self.fetch_and_update_conversation_details_panel(ctx); } ctx.notify(); } @@ -25700,8 +25732,6 @@ impl View for TerminalView { let appearance = Appearance::as_ref(app); let semantic_selection = SemanticSelection::as_ref(app); let model = self.model.lock(); - let ambient_agent_task_id_for_details_panel = - self.ambient_agent_task_id_for_details_panel_from_model(&model, app); let input_mode = if FeatureFlag::AgentView.is_enabled() && self.agent_view_controller.as_ref(app).is_fullscreen() { @@ -26182,18 +26212,19 @@ impl View for TerminalView { element }; - // Wrap with cloud mode details panel on the right if open - // On WASM, the panel is rendered in the wasm_view instead + // Wrap with conversation details panel on the right if open. + // On WASM, the panel is rendered in the wasm_view instead. + // + // Use the `_from_model` variant since `render` already holds + // `self.model.lock()` and the task-id lookup would otherwise re-lock. let should_show_panel = !cfg!(target_family = "wasm") - && self.is_cloud_mode_details_panel_open - && Self::can_show_cloud_mode_details_ui_for_task_id( - ambient_agent_task_id_for_details_panel, - ); + && self.is_conversation_details_panel_open + && self.can_show_conversation_details_ui_from_model(&model, app); if should_show_panel { // Wrap panel with agent view background for visual consistency let panel_with_background = - Container::new(ChildView::new(&self.cloud_mode_details_panel).finish()) + Container::new(ChildView::new(&self.conversation_details_panel).finish()) .with_background(agent_view_bg_fill(app)) .finish(); diff --git a/app/src/terminal/view/action.rs b/app/src/terminal/view/action.rs index 54a82e92..20286bb5 100644 --- a/app/src/terminal/view/action.rs +++ b/app/src/terminal/view/action.rs @@ -412,7 +412,7 @@ pub enum TerminalAction { EnterCloudAgentView, StartNewAgentConversation, /// Toggle the cloud mode conversation details panel - ToggleCloudModeDetailsPanel, + ToggleConversationDetailsPanel, /// Cancel the ambient agent task while it's loading CancelAmbientAgentTask, OpenInlineHistoryMenu, @@ -701,7 +701,7 @@ impl fmt::Debug for TerminalAction { ExitAgentView => write!(f, "ExitAgentView"), EnterCloudAgentView => write!(f, "EnterCloudAgentView"), StartNewAgentConversation => write!(f, "StartNewAgentConversation"), - ToggleCloudModeDetailsPanel => write!(f, "ToggleCloudModeDetailsPanel"), + ToggleConversationDetailsPanel => write!(f, "ToggleConversationDetailsPanel"), CancelAmbientAgentTask => write!(f, "CancelAmbientAgentTask"), OpenInlineHistoryMenu => write!(f, "OpenInlineHistoryMenu"), OpenModelSelector => write!(f, "OpenModelSelector"), diff --git a/app/src/terminal/view/ambient_agent/view_impl.rs b/app/src/terminal/view/ambient_agent/view_impl.rs index 3742b9ea..5f4690b1 100644 --- a/app/src/terminal/view/ambient_agent/view_impl.rs +++ b/app/src/terminal/view/ambient_agent/view_impl.rs @@ -244,7 +244,7 @@ impl TerminalView { self.pending_cloud_followup_task_id = None; } // Auto-open details panel for local cloud mode once the session is ready. - self.maybe_auto_open_cloud_mode_details_panel(ctx); + self.maybe_auto_open_conversation_details_panel(ctx); // Re-render to hide the loading screen now that the session is ready. ctx.emit(TerminalViewEvent::TerminalViewStateChanged); ctx.notify(); @@ -273,8 +273,8 @@ impl TerminalView { ctx, ); // Refresh the details panel to show failed status - if self.is_cloud_mode_details_panel_open { - self.fetch_and_update_cloud_mode_details_panel(ctx); + if self.is_conversation_details_panel_open { + self.fetch_and_update_conversation_details_panel(ctx); } // Re-render to show the error state in the footer. ctx.emit(TerminalViewEvent::TerminalViewStateChanged); @@ -326,8 +326,8 @@ impl TerminalView { ctx, ); // Refresh the details panel to show cancelled status - if self.is_cloud_mode_details_panel_open { - self.fetch_and_update_cloud_mode_details_panel(ctx); + if self.is_conversation_details_panel_open { + self.fetch_and_update_conversation_details_panel(ctx); } // Re-render to show the cancelled state in the footer. ctx.emit(TerminalViewEvent::TerminalViewStateChanged); @@ -874,52 +874,71 @@ impl TerminalView { } } - /// Fetches task data and updates the cloud mode details panel. - pub(in crate::terminal::view) fn fetch_and_update_cloud_mode_details_panel( + /// Fetches task data and updates the conversation details panel. + /// + /// Prefers cloud `AmbientAgentTask` data when this terminal view has an + /// associated task ID. Otherwise falls back to populating the panel from + /// the active local `AIConversation`, so the same panel can surface + /// conversation metadata for non-cloud Warp Agent runs (APP-3595). + pub(in crate::terminal::view) fn fetch_and_update_conversation_details_panel( &mut self, ctx: &mut ViewContext, ) { - let Some(task_id) = self.ambient_agent_task_id_for_details_panel(ctx) else { - log::warn!("fetch_and_update_cloud_mode_details_panel called without task_id"); - return; - }; + if let Some(task_id) = self.ambient_agent_task_id_for_details_panel(ctx) { + let task = crate::ai::agent_conversations_model::AgentConversationsModel::handle(ctx) + .update(ctx, |model, ctx| { + model.get_or_async_fetch_task_data(&task_id, ctx) + }); - let task = crate::ai::agent_conversations_model::AgentConversationsModel::handle(ctx) - .update(ctx, |model, ctx| { - model.get_or_async_fetch_task_data(&task_id, ctx) + let data = task + .as_ref() + .map(|task| ConversationDetailsData::from_task(task, None, None, ctx)) + .unwrap_or_else(|| ConversationDetailsData::from_task_id(task_id)); + self.conversation_details_panel.update(ctx, |panel, ctx| { + panel.set_conversation_details(data, ctx); }); + return; + } - let data = task - .as_ref() - .map(|task| ConversationDetailsData::from_task(task, None, None, ctx)) - .unwrap_or_else(|| ConversationDetailsData::from_task_id(task_id)); - self.cloud_mode_details_panel.update(ctx, |panel, ctx| { - panel.set_conversation_details(data, ctx); - }); + // No backing cloud task — populate from the active local conversation, if any. + let view_id = self.id(); + let history_model = BlocklistAIHistoryModel::handle(ctx); + let data = history_model + .as_ref(ctx) + .active_conversation(view_id) + .map(|conversation| ConversationDetailsData::from_conversation(conversation, ctx)); + + if let Some(data) = data { + self.conversation_details_panel.update(ctx, |panel, ctx| { + panel.set_conversation_details(data, ctx); + }); + } } - pub(in crate::terminal::view) fn refresh_cloud_mode_details_panel_if_open( + pub(in crate::terminal::view) fn refresh_conversation_details_panel_if_open( &mut self, ctx: &mut ViewContext, ) { - if self.is_cloud_mode_details_panel_open && self.can_show_cloud_mode_details_ui(ctx) { - self.fetch_and_update_cloud_mode_details_panel(ctx); + if self.is_conversation_details_panel_open && self.can_show_conversation_details_ui(ctx) { + self.fetch_and_update_conversation_details_panel(ctx); ctx.notify(); } } - /// Auto-opens the cloud mode details panel once. - /// This is used for local cloud mode sessions (after SessionReady) and shared ambient sessions (after join). - pub(in crate::terminal::view) fn maybe_auto_open_cloud_mode_details_panel( + /// Auto-opens the conversation details panel once for cloud mode runs. + /// This is used for local cloud mode sessions (after `SessionReady`) and + /// shared ambient sessions (after join). Local non-cloud conversations + /// require an explicit user click on the pane-header toggle button. + pub(in crate::terminal::view) fn maybe_auto_open_conversation_details_panel( &mut self, ctx: &mut ViewContext, ) { - if self.has_auto_opened_cloud_mode_details_panel { + if self.has_auto_opened_conversation_details_panel { return; } - self.is_cloud_mode_details_panel_open = true; - self.has_auto_opened_cloud_mode_details_panel = true; - self.fetch_and_update_cloud_mode_details_panel(ctx); + self.is_conversation_details_panel_open = true; + self.has_auto_opened_conversation_details_panel = true; + self.fetch_and_update_conversation_details_panel(ctx); ctx.notify(); } } diff --git a/app/src/terminal/view/pane_impl.rs b/app/src/terminal/view/pane_impl.rs index 48b24577..160d86ec 100644 --- a/app/src/terminal/view/pane_impl.rs +++ b/app/src/terminal/view/pane_impl.rs @@ -425,34 +425,35 @@ impl TerminalView { let mut icon_button_count: u32 = 0; - if FeatureFlag::CloudMode.is_enabled() { - let is_waiting_for_session = self + // Cloud-mode-only ambient agent cancel button is shown while we're waiting + // for the session to be ready. + let is_waiting_for_session = FeatureFlag::CloudMode.is_enabled() + && self .ambient_agent_view_model .as_ref() .is_some_and(|model| model.as_ref(app).is_waiting_for_session()); - let button_element = if is_waiting_for_session { - Some(self.render_ambient_agent_cancel_button(app)) - } else if self.can_show_cloud_mode_details_ui(app) { - #[cfg(not(target_arch = "wasm32"))] - { - Some(self.render_cloud_mode_details_toggle_button(app)) - } - #[cfg(target_arch = "wasm32")] - { - None - } - } else { + let button_element = if is_waiting_for_session { + Some(self.render_ambient_agent_cancel_button(app)) + } else if self.can_show_conversation_details_ui(app) { + #[cfg(not(target_arch = "wasm32"))] + { + Some(self.render_conversation_details_toggle_button(app)) + } + #[cfg(target_arch = "wasm32")] + { None - }; + } + } else { + None + }; - if let Some(button) = button_element { - icon_button_count += 1; - if let Some(existing) = left_of_overflow { - left_of_overflow = - Some(Flex::row().with_child(existing).with_child(button).finish()); - } else { - left_of_overflow = Some(button); - } + if let Some(button) = button_element { + icon_button_count += 1; + if let Some(existing) = left_of_overflow { + left_of_overflow = + Some(Flex::row().with_child(existing).with_child(button).finish()); + } else { + left_of_overflow = Some(button); } } @@ -772,13 +773,13 @@ impl TerminalView { .finish() } - /// Render the info button for toggling the cloud mode details panel. + /// Render the info button for toggling the conversation details panel. /// Only available on non-WASM platforms (WASM uses a per-window button instead). #[cfg(not(target_arch = "wasm32"))] - fn render_cloud_mode_details_toggle_button(&self, app: &AppContext) -> Box { + fn render_conversation_details_toggle_button(&self, app: &AppContext) -> Box { let appearance = Appearance::as_ref(app); let theme = appearance.theme(); - let is_open = self.is_cloud_mode_details_panel_open; + let is_open = self.is_conversation_details_panel_open; let ui_builder = appearance.ui_builder().clone(); // Use main text color when panel is open (hover-like appearance), sub color when closed @@ -792,7 +793,7 @@ impl TerminalView { appearance, icons::Icon::Info, is_open, // show active background when panel is open - self.cloud_mode_details_panel_toggle_mouse_state.clone(), + self.conversation_details_panel_toggle_mouse_state.clone(), icon_color, ); @@ -818,7 +819,7 @@ impl TerminalView { .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action::>( - PaneHeaderAction::CustomAction(TerminalAction::ToggleCloudModeDetailsPanel), + PaneHeaderAction::CustomAction(TerminalAction::ToggleConversationDetailsPanel), ); }) .finish() diff --git a/app/src/terminal/view/shared_session/view_impl.rs b/app/src/terminal/view/shared_session/view_impl.rs index 5d069e9f..20bbcd14 100644 --- a/app/src/terminal/view/shared_session/view_impl.rs +++ b/app/src/terminal/view/shared_session/view_impl.rs @@ -705,7 +705,7 @@ impl TerminalView { if FeatureFlag::CloudMode.is_enabled() && matches!(source_type, SessionSourceType::AmbientAgent { .. }) { - self.maybe_auto_open_cloud_mode_details_panel(ctx); + self.maybe_auto_open_conversation_details_panel(ctx); } send_telemetry_from_ctx!( @@ -810,7 +810,7 @@ impl TerminalView { model.mark_task_execution_ended(task_id, ctx); }); } - self.refresh_cloud_mode_details_panel_if_open(ctx); + self.refresh_conversation_details_panel_if_open(ctx); if !FeatureFlag::HandoffCloudCloud.is_enabled() || !FeatureFlag::CloudModeSetupV2.is_enabled() || self.conversation_ended_tombstone_view_id.is_some() diff --git a/app/src/terminal/view/shared_session/view_impl_test.rs b/app/src/terminal/view/shared_session/view_impl_test.rs index a5b84ca7..b5d334be 100644 --- a/app/src/terminal/view/shared_session/view_impl_test.rs +++ b/app/src/terminal/view/shared_session/view_impl_test.rs @@ -848,10 +848,10 @@ fn test_on_ambient_agent_execution_ended_refreshes_open_details_panel_to_termina model.enter_viewing_existing_session(task_id, ctx); }); - view.is_cloud_mode_details_panel_open = true; - view.fetch_and_update_cloud_mode_details_panel(ctx); + view.is_conversation_details_panel_open = true; + view.fetch_and_update_conversation_details_panel(ctx); assert_eq!( - view.cloud_mode_details_panel + view.conversation_details_panel .as_ref(ctx) .task_display_status_for_test(), Some(AgentRunDisplayStatus::TaskInProgress) @@ -859,7 +859,7 @@ fn test_on_ambient_agent_execution_ended_refreshes_open_details_panel_to_termina view.on_ambient_agent_execution_ended(ctx); assert_eq!( - view.cloud_mode_details_panel + view.conversation_details_panel .as_ref(ctx) .task_display_status_for_test(), Some(AgentRunDisplayStatus::ConversationSucceeded)