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 <oz-agent@warp.dev>
This commit is contained in:
Roland Huang
2026-05-04 17:57:52 -05:00
committed by GitHub
parent 39ff0d25a6
commit 564ea2ae58
8 changed files with 241 additions and 119 deletions

View File

@@ -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;

View File

@@ -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 {

View File

@@ -2748,18 +2748,21 @@ pub struct TerminalView {
ambient_agent_view_model: Option<ModelHandle<ambient_agent::AmbientAgentViewModel>>,
pending_cloud_followup_task_id: Option<AmbientAgentTaskId>,
/// 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<crate::ai::conversation_details_panel::ConversationDetailsPanel>,
/// 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<AmbientAgentTaskId>) -> 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();

View File

@@ -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"),

View File

@@ -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<Self>,
) {
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<Self>,
) {
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<Self>,
) {
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();
}
}

View File

@@ -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<dyn Element> {
fn render_conversation_details_toggle_button(&self, app: &AppContext) -> Box<dyn Element> {
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<TerminalAction, TerminalAction>>(
PaneHeaderAction::CustomAction(TerminalAction::ToggleCloudModeDetailsPanel),
PaneHeaderAction::CustomAction(TerminalAction::ToggleConversationDetailsPanel),
);
})
.finish()

View File

@@ -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()

View File

@@ -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)