Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions app/src/ai/agent_conversations_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ impl ConversationOrTask<'_> {
}

/// Resolve the effective execution harness for this run.
pub fn harness(&self) -> Option<Harness> {
pub fn harness(&self, app: &AppContext) -> Option<Harness> {
match self {
ConversationOrTask::Task(task) => {
task.agent_config_snapshot.as_ref().and_then(|config| {
Expand All @@ -618,7 +618,10 @@ impl ConversationOrTask<'_> {
.or(Some(Harness::Oz))
})
}
ConversationOrTask::Conversation(_) => Some(Harness::Oz),
ConversationOrTask::Conversation(metadata) => BlocklistAIHistoryModel::as_ref(app)
.get_server_conversation_metadata(&metadata.nav_data.id)
.map(|m| Harness::from(m.harness))
.or(Some(Harness::Oz)),
}
}

Expand Down Expand Up @@ -741,10 +744,10 @@ impl ConversationOrTask<'_> {
}

/// Check if this item matches the harness filter.
fn matches_harness(&self, harness_filter: &HarnessFilter) -> bool {
fn matches_harness(&self, harness_filter: &HarnessFilter, app: &AppContext) -> bool {
match harness_filter {
HarnessFilter::All => true,
HarnessFilter::Specific(h) => self.harness() == Some(*h),
HarnessFilter::Specific(h) => self.harness(app) == Some(*h),
}
}

Expand Down Expand Up @@ -1571,7 +1574,8 @@ impl AgentConversationsModel {
};

let harness_filter_value = filters.harness;
let harness_filter = move |t: &ConversationOrTask| t.matches_harness(&harness_filter_value);
let harness_filter =
move |t: &ConversationOrTask| t.matches_harness(&harness_filter_value, app);

let tasks_iter = self.tasks.values().map(ConversationOrTask::Task);
let conversations_iter = self
Expand Down
2 changes: 1 addition & 1 deletion app/src/ai/agent_management/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1837,7 +1837,7 @@ impl AgentManagementView {
}

if FeatureFlag::AgentHarness.is_enabled() {
if let Some(harness) = card_data.harness() {
if let Some(harness) = card_data.harness(app) {
metadata_parts.push(format!(
"Harness: {}",
harness_display::display_name(harness)
Expand Down
13 changes: 12 additions & 1 deletion app/src/terminal/view/pane_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use super::ambient_agent::is_cloud_agent_pre_first_exchange;
use super::shared_session::adapter::Kind as SharedSessionKind;
use super::{Event, PaneConfiguration, TerminalAction, TerminalViewState, Viewer};
use crate::ai::agent::conversation::{AIConversation, ConversationStatus};
use crate::ai::agent::conversation::{
AIConversation, ConversationStatus, ServerAIConversationMetadata,
};
use crate::ai::blocklist::agent_view::agent_view_bg_fill;
use crate::ai::blocklist::agent_view::orchestration_conversation_links::parent_conversation_navigation_card;
use crate::ai::blocklist::agent_view::render_orchestration_breadcrumbs;
Expand Down Expand Up @@ -1018,6 +1020,15 @@ impl TerminalView {
})
}

/// Server metadata for the selected conversation, if any.
pub fn selected_conversation_server_metadata<'a>(
&'a self,
ctx: &'a AppContext,
) -> Option<&'a ServerAIConversationMetadata> {
self.selected_conversation_for_user_facing_chrome(ctx)
.and_then(AIConversation::server_metadata)
}

pub fn selected_conversation_latest_user_prompt_for_tab_name(
&self,
ctx: &AppContext,
Expand Down
92 changes: 56 additions & 36 deletions app/src/ui_components/agent_icon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use warpui::AppContext;
use warpui::SingletonEntity;

use crate::ai::agent::conversation::ConversationStatus;
use crate::ai::agent_conversations_model::ConversationOrTask;
use crate::ai::agent_conversations_model::{AgentConversationsModel, ConversationOrTask};
use crate::ai::blocklist::BlocklistAIHistoryModel;
use crate::terminal::cli_agent_sessions::listener::agent_supports_rich_status;
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
use crate::terminal::view::TerminalView;
Expand All @@ -24,26 +25,48 @@ use crate::ui_components::icon_with_status::IconWithStatusVariant;
/// not an agent surface (plain terminal / shell / empty conversation).
///
/// Resolution order:
/// 1. A [`CLIAgentSessionsModel`] session with a known agent (observed reality) wins.
/// Plugin-backed sessions surface rich status; command-detected sessions don't.
/// 2. An ambient agent with a selected third-party harness uses the harness's CLI brand
/// even before the harness CLI has started running in the sandbox.
/// 3. A selected conversation or ambient Oz run falls back to the Oz agent variant.
/// 1. A [`CLIAgentSessionsModel`] session with a known agent wins. Plugin-backed sessions
/// surface rich status; command-detected sessions don't.
/// 2. A task-backed run defers to [`conversation_or_task_agent_icon_variant`] so the
/// terminal chrome and the matching conversation list card stay in lockstep.
/// 3. Live ambient pre-dispatch or a selected local conversation falls through to the
/// no-task waterfall.
/// 4. Everything else returns `None` so the caller renders a plain-terminal indicator.
pub(crate) fn terminal_view_agent_icon_variant(
terminal_view: &TerminalView,
app: &AppContext,
) -> Option<IconWithStatusVariant> {
let cli_agent_session = CLIAgentSessionsModel::as_ref(app).session(terminal_view.id());

// Resolve the ambient task id from [`TerminalView::ambient_agent_task_id_for_details_panel`],
// falling back to the selected conversation's server metadata for restored cloud transcripts.
let ambient_task_id = terminal_view
.ambient_agent_task_id_for_details_panel(app)
.or_else(|| {
terminal_view
.selected_conversation_server_metadata(app)
.and_then(|m| m.ambient_agent_task_id)
});
let task_data = ambient_task_id
.and_then(|task_id| AgentConversationsModel::as_ref(app).get_task_data(&task_id));

// Defer to the card helper when we have task data and no CLI session takes precedence.
if cli_agent_session.is_none() {
if let Some(task) = task_data.as_ref() {
return conversation_or_task_agent_icon_variant(&ConversationOrTask::Task(task), app);
}
}

let is_ambient = terminal_view.is_ambient_agent_session(app) || ambient_task_id.is_some();
let inputs = TerminalIconInputs {
is_ambient: terminal_view.is_ambient_agent_session(app),
is_ambient,
cli_session: cli_agent_session.map(|session| CLISessionInputs {
agent: session.agent,
has_listener: session.listener.is_some(),
status: session.status.to_conversation_status(),
supports_rich_status: agent_supports_rich_status(&session.agent),
}),
ambient_selected_third_party_cli_agent: terminal_view
selected_third_party_cli_agent: terminal_view
.ambient_agent_view_model()
.and_then(|model| model.as_ref(app).selected_third_party_cli_agent()),
selected_conversation_status: terminal_view.selected_conversation_status_for_display(app),
Expand All @@ -56,33 +79,30 @@ pub(crate) fn terminal_view_agent_icon_variant(

/// Returns the agent-icon variant for a [`ConversationOrTask`] card row.
///
/// Task rows resolve their harness from [`ConversationOrTask::harness`]; conversation
/// rows have no harness signal and always render as local Oz per the product spec.
/// Both tasks and conversations resolve their harness through [`ConversationOrTask::harness`].
pub(crate) fn conversation_or_task_agent_icon_variant(
src: &ConversationOrTask<'_>,
app: &AppContext,
) -> Option<IconWithStatusVariant> {
let status = src.status(app);
Some(match src {
ConversationOrTask::Task(_) => {
agent_icon_variant_for_task(src.harness().unwrap_or(Harness::Oz), status)
}
ConversationOrTask::Conversation(_) => IconWithStatusVariant::OzAgent {
status: Some(status),
is_ambient: false,
},
})
let harness = src.harness(app).unwrap_or(Harness::Oz);
let is_ambient = match src {
ConversationOrTask::Task(_) => true,
ConversationOrTask::Conversation(metadata) => BlocklistAIHistoryModel::as_ref(app)
.get_server_conversation_metadata(&metadata.nav_data.id)
.is_some_and(|m| m.ambient_agent_task_id.is_some()),
};
Some(agent_icon_variant_for_run(harness, status, is_ambient))
}

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

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

/// Pure task-card logic: maps a [`Harness`] and the task's current status into an
/// [`IconWithStatusVariant`]. Task cards are always ambient. Falls back to the Oz
/// variant for [`Harness::Oz`] and [`Harness::Unknown`], the latter so a future-server
/// harness this client doesn't recognize doesn't render an unbranded gray circle.
fn agent_icon_variant_for_task(
/// Pure run-card logic: maps a [`Harness`], status, and ambient flag into an
/// [`IconWithStatusVariant`]. Falls back to the Oz variant for [`Harness::Oz`] and
/// [`Harness::Unknown`], the latter so a future-server harness this client doesn't
/// recognize doesn't render an unbranded gray circle.
fn agent_icon_variant_for_run(
harness: Harness,
status: ConversationStatus,
is_ambient: bool,
) -> IconWithStatusVariant {
let cli_agent =
CLIAgent::from_harness(harness).filter(|agent| !matches!(agent, CLIAgent::Unknown));
match cli_agent {
Some(agent) => IconWithStatusVariant::CLIAgent {
agent,
status: Some(status),
is_ambient: true,
is_ambient,
},
None => IconWithStatusVariant::OzAgent {
status: Some(status),
is_ambient: true,
is_ambient,
},
}
}
Expand Down
Loading
Loading