Skip to content

Commit 34d311d

Browse files
authored
use correct cloud agent icons for 3p conversation transcripts (#10148)
## Description <!-- Please remember to add your design buddy onto the PR for review, if it contains any UI changes! --> We weren't properly checking/respecting 3p agent harness information for cloud conversation transcripts when deciding which icon to display in the conversation list, vertical tabs, and pane header. ## Screenshots / Videos <!-- Attach screenshots or a short video demonstrating the change, where appropriate. Remove this section if it is not relevant to your PR. --> ![image.png](https://app.graphite.com/user-attachments/assets/96a893c8-bee1-46f7-b2cf-643276e9c763.png) ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode
1 parent 8e837a0 commit 34d311d

5 files changed

Lines changed: 126 additions & 67 deletions

File tree

app/src/ai/agent_conversations_model.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ impl ConversationOrTask<'_> {
607607
}
608608

609609
/// Resolve the effective execution harness for this run.
610-
pub fn harness(&self) -> Option<Harness> {
610+
pub fn harness(&self, app: &AppContext) -> Option<Harness> {
611611
match self {
612612
ConversationOrTask::Task(task) => {
613613
task.agent_config_snapshot.as_ref().and_then(|config| {
@@ -618,7 +618,10 @@ impl ConversationOrTask<'_> {
618618
.or(Some(Harness::Oz))
619619
})
620620
}
621-
ConversationOrTask::Conversation(_) => Some(Harness::Oz),
621+
ConversationOrTask::Conversation(metadata) => BlocklistAIHistoryModel::as_ref(app)
622+
.get_server_conversation_metadata(&metadata.nav_data.id)
623+
.map(|m| Harness::from(m.harness))
624+
.or(Some(Harness::Oz)),
622625
}
623626
}
624627

@@ -741,10 +744,10 @@ impl ConversationOrTask<'_> {
741744
}
742745

743746
/// Check if this item matches the harness filter.
744-
fn matches_harness(&self, harness_filter: &HarnessFilter) -> bool {
747+
fn matches_harness(&self, harness_filter: &HarnessFilter, app: &AppContext) -> bool {
745748
match harness_filter {
746749
HarnessFilter::All => true,
747-
HarnessFilter::Specific(h) => self.harness() == Some(*h),
750+
HarnessFilter::Specific(h) => self.harness(app) == Some(*h),
748751
}
749752
}
750753

@@ -1572,7 +1575,8 @@ impl AgentConversationsModel {
15721575
};
15731576

15741577
let harness_filter_value = filters.harness;
1575-
let harness_filter = move |t: &ConversationOrTask| t.matches_harness(&harness_filter_value);
1578+
let harness_filter =
1579+
move |t: &ConversationOrTask| t.matches_harness(&harness_filter_value, app);
15761580

15771581
let tasks_iter = self.tasks.values().map(ConversationOrTask::Task);
15781582
let conversations_iter = self

app/src/ai/agent_management/view.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1837,7 +1837,7 @@ impl AgentManagementView {
18371837
}
18381838

18391839
if FeatureFlag::AgentHarness.is_enabled() {
1840-
if let Some(harness) = card_data.harness() {
1840+
if let Some(harness) = card_data.harness(app) {
18411841
metadata_parts.push(format!(
18421842
"Harness: {}",
18431843
harness_display::display_name(harness)

app/src/terminal/view/pane_impl.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
use super::ambient_agent::is_cloud_agent_pre_first_exchange;
44
use super::shared_session::adapter::Kind as SharedSessionKind;
55
use super::{Event, PaneConfiguration, TerminalAction, TerminalViewState, Viewer};
6-
use crate::ai::agent::conversation::{AIConversation, ConversationStatus};
6+
use crate::ai::agent::conversation::{
7+
AIConversation, ConversationStatus, ServerAIConversationMetadata,
8+
};
79
use crate::ai::blocklist::agent_view::agent_view_bg_fill;
810
use crate::ai::blocklist::agent_view::orchestration_conversation_links::parent_conversation_navigation_card;
911
use crate::ai::blocklist::agent_view::render_orchestration_breadcrumbs;
@@ -1020,6 +1022,15 @@ impl TerminalView {
10201022
})
10211023
}
10221024

1025+
/// Server metadata for the selected conversation, if any.
1026+
pub fn selected_conversation_server_metadata<'a>(
1027+
&'a self,
1028+
ctx: &'a AppContext,
1029+
) -> Option<&'a ServerAIConversationMetadata> {
1030+
self.selected_conversation_for_user_facing_chrome(ctx)
1031+
.and_then(AIConversation::server_metadata)
1032+
}
1033+
10231034
pub fn selected_conversation_latest_user_prompt_for_tab_name(
10241035
&self,
10251036
ctx: &AppContext,

app/src/ui_components/agent_icon.rs

Lines changed: 56 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use warpui::AppContext;
1313
use warpui::SingletonEntity;
1414

1515
use crate::ai::agent::conversation::ConversationStatus;
16-
use crate::ai::agent_conversations_model::ConversationOrTask;
16+
use crate::ai::agent_conversations_model::{AgentConversationsModel, ConversationOrTask};
17+
use crate::ai::blocklist::BlocklistAIHistoryModel;
1718
use crate::terminal::cli_agent_sessions::listener::agent_supports_rich_status;
1819
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
1920
use crate::terminal::view::TerminalView;
@@ -24,26 +25,48 @@ use crate::ui_components::icon_with_status::IconWithStatusVariant;
2425
/// not an agent surface (plain terminal / shell / empty conversation).
2526
///
2627
/// Resolution order:
27-
/// 1. A [`CLIAgentSessionsModel`] session with a known agent (observed reality) wins.
28-
/// Plugin-backed sessions surface rich status; command-detected sessions don't.
29-
/// 2. An ambient agent with a selected third-party harness uses the harness's CLI brand
30-
/// even before the harness CLI has started running in the sandbox.
31-
/// 3. A selected conversation or ambient Oz run falls back to the Oz agent variant.
28+
/// 1. A [`CLIAgentSessionsModel`] session with a known agent wins. Plugin-backed sessions
29+
/// surface rich status; command-detected sessions don't.
30+
/// 2. A task-backed run defers to [`conversation_or_task_agent_icon_variant`] so the
31+
/// terminal chrome and the matching conversation list card stay in lockstep.
32+
/// 3. Live ambient pre-dispatch or a selected local conversation falls through to the
33+
/// no-task waterfall.
3234
/// 4. Everything else returns `None` so the caller renders a plain-terminal indicator.
3335
pub(crate) fn terminal_view_agent_icon_variant(
3436
terminal_view: &TerminalView,
3537
app: &AppContext,
3638
) -> Option<IconWithStatusVariant> {
3739
let cli_agent_session = CLIAgentSessionsModel::as_ref(app).session(terminal_view.id());
40+
41+
// Resolve the ambient task id from [`TerminalView::ambient_agent_task_id_for_details_panel`],
42+
// falling back to the selected conversation's server metadata for restored cloud transcripts.
43+
let ambient_task_id = terminal_view
44+
.ambient_agent_task_id_for_details_panel(app)
45+
.or_else(|| {
46+
terminal_view
47+
.selected_conversation_server_metadata(app)
48+
.and_then(|m| m.ambient_agent_task_id)
49+
});
50+
let task_data = ambient_task_id
51+
.and_then(|task_id| AgentConversationsModel::as_ref(app).get_task_data(&task_id));
52+
53+
// Defer to the card helper when we have task data and no CLI session takes precedence.
54+
if cli_agent_session.is_none() {
55+
if let Some(task) = task_data.as_ref() {
56+
return conversation_or_task_agent_icon_variant(&ConversationOrTask::Task(task), app);
57+
}
58+
}
59+
60+
let is_ambient = terminal_view.is_ambient_agent_session(app) || ambient_task_id.is_some();
3861
let inputs = TerminalIconInputs {
39-
is_ambient: terminal_view.is_ambient_agent_session(app),
62+
is_ambient,
4063
cli_session: cli_agent_session.map(|session| CLISessionInputs {
4164
agent: session.agent,
4265
has_listener: session.listener.is_some(),
4366
status: session.status.to_conversation_status(),
4467
supports_rich_status: agent_supports_rich_status(&session.agent),
4568
}),
46-
ambient_selected_third_party_cli_agent: terminal_view
69+
selected_third_party_cli_agent: terminal_view
4770
.ambient_agent_view_model()
4871
.and_then(|model| model.as_ref(app).selected_third_party_cli_agent()),
4972
selected_conversation_status: terminal_view.selected_conversation_status_for_display(app),
@@ -56,33 +79,30 @@ pub(crate) fn terminal_view_agent_icon_variant(
5679

5780
/// Returns the agent-icon variant for a [`ConversationOrTask`] card row.
5881
///
59-
/// Task rows resolve their harness from [`ConversationOrTask::harness`]; conversation
60-
/// rows have no harness signal and always render as local Oz per the product spec.
82+
/// Both tasks and conversations resolve their harness through [`ConversationOrTask::harness`].
6183
pub(crate) fn conversation_or_task_agent_icon_variant(
6284
src: &ConversationOrTask<'_>,
6385
app: &AppContext,
6486
) -> Option<IconWithStatusVariant> {
6587
let status = src.status(app);
66-
Some(match src {
67-
ConversationOrTask::Task(_) => {
68-
agent_icon_variant_for_task(src.harness().unwrap_or(Harness::Oz), status)
69-
}
70-
ConversationOrTask::Conversation(_) => IconWithStatusVariant::OzAgent {
71-
status: Some(status),
72-
is_ambient: false,
73-
},
74-
})
88+
let harness = src.harness(app).unwrap_or(Harness::Oz);
89+
let is_ambient = match src {
90+
ConversationOrTask::Task(_) => true,
91+
ConversationOrTask::Conversation(metadata) => BlocklistAIHistoryModel::as_ref(app)
92+
.get_server_conversation_metadata(&metadata.nav_data.id)
93+
.is_some_and(|m| m.ambient_agent_task_id.is_some()),
94+
};
95+
Some(agent_icon_variant_for_run(harness, status, is_ambient))
7596
}
7697

7798
/// Primitive inputs to the terminal-view waterfall, gathered once from the live
78-
/// [`TerminalView`] / [`AppContext`]. Keeping the decision logic in terms of these
79-
/// primitives makes it testable without a live app.
99+
/// [`TerminalView`] / [`AppContext`].
80100
struct TerminalIconInputs {
81101
is_ambient: bool,
82102
cli_session: Option<CLISessionInputs>,
83-
/// The CLI agent corresponding to the currently selected cloud harness, when the selection
84-
/// is a third-party (non-Oz) harness. `None` for Oz or when no harness is selected.
85-
ambient_selected_third_party_cli_agent: Option<CLIAgent>,
103+
/// Third-party CLI agent for a live ambient run before task data is available (e.g.
104+
/// Claude pre-dispatch). `None` otherwise; task-derived harnesses are handled upstream.
105+
selected_third_party_cli_agent: Option<CLIAgent>,
86106
/// The conversation status that the terminal view would surface in its status-icon slot.
87107
selected_conversation_status: Option<ConversationStatus>,
88108
/// Whether the terminal view currently has a selected conversation (ambient or local).
@@ -122,13 +142,12 @@ fn agent_icon_variant_from_terminal_inputs(
122142
});
123143
}
124144

125-
// 2. Ambient agent with a selected third-party harness. Render the harness's brand
126-
// circle immediately once the user commits, even before the harness CLI starts
127-
// running in the sandbox. `Unknown` is filtered to avoid rendering an unbranded
128-
// gray circle for a harness this client doesn't recognize.
145+
// 2. Live ambient run with a third-party harness selected, before task data is
146+
// available (e.g. Claude pre-dispatch). `Unknown` is filtered so an unrecognized
147+
// harness doesn't render as an unbranded gray circle.
129148
if inputs.is_ambient {
130149
if let Some(agent) = inputs
131-
.ambient_selected_third_party_cli_agent
150+
.selected_third_party_cli_agent
132151
.filter(|agent| !matches!(agent, CLIAgent::Unknown))
133152
{
134153
return Some(IconWithStatusVariant::CLIAgent {
@@ -150,25 +169,26 @@ fn agent_icon_variant_from_terminal_inputs(
150169
None
151170
}
152171

153-
/// Pure task-card logic: maps a [`Harness`] and the task's current status into an
154-
/// [`IconWithStatusVariant`]. Task cards are always ambient. Falls back to the Oz
155-
/// variant for [`Harness::Oz`] and [`Harness::Unknown`], the latter so a future-server
156-
/// harness this client doesn't recognize doesn't render an unbranded gray circle.
157-
fn agent_icon_variant_for_task(
172+
/// Pure run-card logic: maps a [`Harness`], status, and ambient flag into an
173+
/// [`IconWithStatusVariant`]. Falls back to the Oz variant for [`Harness::Oz`] and
174+
/// [`Harness::Unknown`], the latter so a future-server harness this client doesn't
175+
/// recognize doesn't render an unbranded gray circle.
176+
fn agent_icon_variant_for_run(
158177
harness: Harness,
159178
status: ConversationStatus,
179+
is_ambient: bool,
160180
) -> IconWithStatusVariant {
161181
let cli_agent =
162182
CLIAgent::from_harness(harness).filter(|agent| !matches!(agent, CLIAgent::Unknown));
163183
match cli_agent {
164184
Some(agent) => IconWithStatusVariant::CLIAgent {
165185
agent,
166186
status: Some(status),
167-
is_ambient: true,
187+
is_ambient,
168188
},
169189
None => IconWithStatusVariant::OzAgent {
170190
status: Some(status),
171-
is_ambient: true,
191+
is_ambient,
172192
},
173193
}
174194
}

0 commit comments

Comments
 (0)