first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+305 -308
View File
@@ -8,62 +8,9 @@ use galaxyui::scene::DropShadow;
use galaxyui::ui_components::button::ButtonVariant;
use lazy_static::lazy_static;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::vec2f;
use settings::Setting;
use siphasher::sip::SipHasher;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent_conversations_model::{
AgentConversationsModel, AgentConversationsModelEvent, AgentManagementFilters, ArtifactFilter,
ConversationOrTask, CreatedOnFilter, CreatorFilter, EnvironmentFilter, HarnessFilter,
OwnerFilter, SessionStatus, SourceFilter, StatusFilter,
};
use crate::ai::agent_management::agent_type_selector::{
AgentType, AgentTypeSelector, AgentTypeSelectorEvent,
};
use crate::ai::agent_management::cloud_setup_guide_view::{
CloudSetupGuideEvent, CloudSetupGuideView,
};
use crate::ai::agent_management::details_action_buttons::{
ActionButtonsConfig, AgentDetailsButtonEvent, ConversationActionButtonsRow,
};
use crate::ai::agent_management::telemetry::{
AgentManagementTelemetryEvent, ArtifactType, FilterType, OpenedFrom,
};
use crate::ai::ambient_agents::AmbientAgentTaskId;
use crate::ai::ambient_agents::{cancel_task_with_toast, AgentSource};
use crate::ai::artifacts::{Artifact, ArtifactButtonsRow, ArtifactButtonsRowEvent};
use crate::ai::blocklist::history_model::BlocklistAIHistoryModel;
use crate::ai::conversation_details_panel::{
ConversationDetailsData, ConversationDetailsPanel, ConversationDetailsPanelEvent,
};
use crate::ai::conversation_status_ui::render_status_element;
use crate::ai::harness_display;
use crate::app_state::PersistedAgentManagementFilters;
use crate::appearance::Appearance;
use crate::auth::AuthStateProvider;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys,
PropagateHorizontalNavigationKeys, SingleLineEditorOptions, TextOptions,
};
use crate::menu::{MenuItem, MenuItemFields};
use crate::notebooks::NotebookId;
use crate::settings::ai::AISettings;
use crate::ui_components::avatar::{Avatar, AvatarContent};
use crate::util::time_format::format_approx_duration_from_now_utc;
use crate::view_components::action_button::{
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
};
use crate::view_components::compactible_action_button::{
CompactibleActionButton, MEDIUM_SIZE_SWITCH_THRESHOLD,
};
use crate::view_components::dropdown::{Dropdown, DropdownAction, DropdownStyle};
use crate::view_components::DismissibleToast;
use crate::view_components::FilterableDropdown;
use crate::workflows::WorkflowType;
use crate::workspace::{ForkedConversationDestination, ToastStack};
use crate::workspace::{RestoreConversationLayout, WorkspaceAction};
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::{send_telemetry_from_ctx, AgentModeEntrypoint};
use galaxy_cli::agent::Harness;
use galaxy_core::ui::icons::Icon;
use galaxy_core::ui::theme::color::internal_colors;
use galaxy_core::ui::theme::Fill;
@@ -79,15 +26,71 @@ use galaxyui::elements::{
SizeConstraintCondition, SizeConstraintSwitch, Stack, Text, Wrap,
};
use galaxyui::fonts::{Properties, Weight};
use galaxyui::keymap::FixedBinding;
use galaxyui::platform::Cursor;
use galaxyui::ui_components::components::UiComponent;
use galaxyui::ui_components::components::UiComponentStyles;
use galaxyui::ui_components::components::{UiComponent, UiComponentStyles};
use galaxyui::{
keymap::FixedBinding, Action, AppContext, Entity, FocusContext, ModelHandle, SingletonEntity,
TypedActionView, View, ViewContext, ViewHandle, WeakViewHandle,
AppContext, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use pathfinder_geometry::vector::vec2f;
use settings::Setting;
use crate::ai::agent::conversation::AIConversationId;
use crate::ai::agent_conversations_model::{
AgentConversationEntry, AgentConversationEntryId, AgentConversationNavigationSubject,
AgentConversationsModel, AgentConversationsModelEvent, AgentManagementFilters, ArtifactFilter,
ConversationUpdateKind, CreatedOnFilter, CreatorFilter, EnvironmentFilter, HarnessFilter,
OwnerFilter, SessionStatus, SourceFilter, StatusFilter,
};
use crate::ai::agent_management::agent_type_selector::{
AgentType, AgentTypeSelector, AgentTypeSelectorEvent,
};
use crate::ai::agent_management::cloud_setup_guide_view::{
CloudSetupGuideEvent, CloudSetupGuideView,
};
use crate::ai::agent_management::details_action_buttons::{
ActionButtonsConfig, AgentDetailsButtonEvent, ConversationActionButtonsRow,
};
use crate::ai::agent_management::telemetry::{
AgentManagementTelemetryEvent, ArtifactType, FilterType, OpenedFrom,
};
use crate::ai::ambient_agents::{cancel_task_with_toast, AgentSource};
use crate::ai::artifacts::{Artifact, ArtifactButtonsRow, ArtifactButtonsRowEvent};
use crate::ai::blocklist::format_credits;
use crate::ai::conversation_details_panel::{
ConversationDetailsData, ConversationDetailsPanel, ConversationDetailsPanelEvent,
};
use crate::ai::harness_availability::HarnessAvailabilityModel;
use crate::ai::harness_display;
use crate::app_state::PersistedAgentManagementFilters;
use crate::appearance::Appearance;
use crate::auth::AuthStateProvider;
use crate::editor::{
EditorView, Event as EditorEvent, PropagateAndNoOpNavigationKeys,
PropagateHorizontalNavigationKeys, SingleLineEditorOptions, TextOptions,
};
use crate::menu::{MenuItem, MenuItemFields};
use crate::notebooks::NotebookId;
use crate::settings::ai::AISettings;
use crate::ui_components::agent_icon::agent_conversation_entry_icon_variant;
use crate::ui_components::avatar::{Avatar, AvatarContent};
use crate::ui_components::icon_with_status::render_icon_with_status;
use crate::util::time_format::format_approx_duration_from_now_utc;
use crate::view_components::action_button::{
ActionButton, ButtonSize, NakedTheme, PrimaryTheme, SecondaryTheme,
};
use crate::view_components::compactible_action_button::{
CompactibleActionButton, MEDIUM_SIZE_SWITCH_THRESHOLD,
};
use crate::view_components::dropdown::{
Dropdown, DropdownAction, DropdownItemAction, DropdownStyle,
};
use crate::view_components::{DismissibleToast, FilterableDropdown};
use crate::workflows::WorkflowType;
use crate::workspace::{
ForkedConversationDestination, RestoreConversationLayout, ToastStack, WorkspaceAction,
};
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::{send_telemetry_from_ctx, AgentModeEntrypoint};
lazy_static! {
static ref HASHER: SipHasher = SipHasher::new_with_keys(0, 0);
@@ -105,8 +108,9 @@ const CARD_CONTENT_PADDING: f32 = 12.;
const CARD_BORDER_RADIUS: f32 = 4.;
const CARD_MARGIN_BOTTOM: f32 = 8.;
const STATUS_ICON_SIZE: f32 = 12.;
const BUTTON_SIZE: f32 = 20.;
/// Total size of the agent icon-with-status component rendered in each card's header row.
const CARD_AGENT_ICON_SIZE: f32 = 24.;
const CREATOR_AVATAR_FONT_SIZE: f32 = 10.;
const SESSION_EXPIRED_TEXT: &str = "Sessions expire after one week and cannot be opened.";
@@ -125,21 +129,7 @@ fn should_show_artifacts(artifacts: &[Artifact]) -> bool {
!artifacts.is_empty() && FeatureFlag::ConversationArtifacts.is_enabled()
}
/// Identifies a card item - either a task ID or a conversation ID
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ManagementCardItemId {
Task(AmbientAgentTaskId),
Conversation(AIConversationId),
}
impl ManagementCardItemId {
fn as_key(&self) -> String {
match self {
ManagementCardItemId::Task(id) => format!("task_{id}"),
ManagementCardItemId::Conversation(id) => format!("conv_{id}"),
}
}
}
pub type ManagementCardItemId = AgentConversationEntryId;
/// Store state for a given task row
struct CardState {
@@ -222,6 +212,13 @@ impl AgentManagementView {
Self::handle_agent_management_model_event,
);
ctx.subscribe_to_model(
&HarnessAvailabilityModel::handle(ctx),
|me, _, _event, ctx| {
me.update_harness_dropdown(ctx);
},
);
let list_state = Self::construct_fresh_list_state(ctx.handle());
let all_filter_button = ctx.add_typed_action_view(|_ctx| {
@@ -496,7 +493,7 @@ impl AgentManagementView {
let make_status_option =
|label: &str, action: AgentManagementViewAction, icon_data: Option<(Icon, Fill)>| {
let mut fields = MenuItemFields::new(label)
.with_on_select_action(DropdownAction::SelectActionAndClose(action));
.with_on_select_action(DropdownAction::select_action_and_close(action));
if let Some((icon, color)) = icon_data {
fields = fields.with_icon(icon).with_override_icon_color(color);
}
@@ -546,7 +543,7 @@ impl AgentManagementView {
}
/// Build the list of source filter items.
fn build_source_dropdown_items() -> Vec<MenuItem<DropdownAction<AgentManagementViewAction>>> {
fn build_source_dropdown_items() -> Vec<MenuItem<DropdownAction>> {
// Build up the sources list
let mut sources = vec![
AgentSource::WebApp,
@@ -564,14 +561,16 @@ impl AgentManagementView {
}
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(DropdownAction::SelectActionAndClose(
AgentManagementViewAction::SetSourceFilter(SourceFilter::All),
)),
MenuItemFields::new("All").with_on_select_action(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetSourceFilter(SourceFilter::All),
),
),
)];
for source in sources {
items.push(MenuItem::Item(
MenuItemFields::new(source.display_name()).with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetSourceFilter(SourceFilter::Specific(source)),
),
),
@@ -601,22 +600,22 @@ impl AgentManagementView {
let items = vec![
MenuItem::Item(MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::All),
),
)),
MenuItem::Item(MenuItemFields::new("Last 24 hours").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Last24Hours),
),
)),
MenuItem::Item(MenuItemFields::new("Past 3 days").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::Past3Days),
),
)),
MenuItem::Item(MenuItemFields::new("Last week").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatedOnFilter(CreatedOnFilter::LastWeek),
),
)),
@@ -635,29 +634,29 @@ impl AgentManagementView {
let items = vec![
MenuItem::Item(MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::All,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::All),
),
)),
MenuItem::Item(MenuItemFields::new("Pull Request").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::PullRequest,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::PullRequest),
),
)),
MenuItem::Item(MenuItemFields::new("Plan").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::Plan,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Plan),
),
)),
MenuItem::Item(MenuItemFields::new("Screenshot").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::Screenshot,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::Screenshot),
),
)),
MenuItem::Item(MenuItemFields::new("File").with_on_select_action(
DropdownAction::SelectActionAndClose(AgentManagementViewAction::SetArtifactFilter(
ArtifactFilter::File,
)),
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetArtifactFilter(ArtifactFilter::File),
),
)),
];
@@ -672,17 +671,27 @@ impl AgentManagementView {
let mut dropdown = Dropdown::new(ctx);
Self::setup_filter_menu(&mut dropdown, "Harness", ctx);
// "All" has no leading icon, matching the Status dropdown's "All" row.
let items = Self::build_harness_dropdown_items(ctx);
dropdown.set_rich_items(items, ctx);
dropdown.set_selected_by_index(0, ctx);
dropdown
}
fn build_harness_dropdown_items(app: &AppContext) -> Vec<MenuItem<DropdownAction>> {
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(DropdownAction::SelectActionAndClose(
AgentManagementViewAction::SetHarnessFilter(HarnessFilter::All),
)),
MenuItemFields::new("All").with_on_select_action(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetHarnessFilter(HarnessFilter::All),
),
),
)];
for harness in [Harness::Oz, Harness::Claude, Harness::Gemini] {
let mut fields = MenuItemFields::new(harness_display::display_name(harness))
let availability = HarnessAvailabilityModel::as_ref(app);
for entry in availability.available_harnesses() {
let harness = entry.harness;
let mut fields = MenuItemFields::new(entry.display_name.clone())
.with_icon(harness_display::icon_for(harness))
.with_on_select_action(DropdownAction::SelectActionAndClose(
.with_on_select_action(DropdownAction::select_action_and_close(
AgentManagementViewAction::SetHarnessFilter(HarnessFilter::Specific(harness)),
));
if let Some(color) = harness_display::brand_color(harness) {
@@ -691,9 +700,7 @@ impl AgentManagementView {
items.push(MenuItem::Item(fields));
}
dropdown.set_rich_items(items, ctx);
dropdown.set_selected_by_index(0, ctx);
dropdown
items
}
fn create_environment_dropdown(
@@ -732,7 +739,7 @@ impl AgentManagementView {
}
// Initialize the dropdown menu for the filter dropdowns (status, source)
fn setup_filter_menu<A: Action + Clone>(
fn setup_filter_menu<A: DropdownItemAction>(
dropdown: &mut Dropdown<A>,
label_prefix: &'static str,
ctx: &mut ViewContext<Dropdown<A>>,
@@ -744,7 +751,7 @@ impl AgentManagementView {
}
// Initialize the dropdown menu for the searchable filter dropdowns (creator)
fn setup_searchable_filter_menu<A: Action + Clone>(
fn setup_searchable_filter_menu<A: DropdownItemAction>(
dropdown: &mut FilterableDropdown<A>,
label_prefix: &'static str,
ctx: &mut ViewContext<FilterableDropdown<A>>,
@@ -755,6 +762,13 @@ impl AgentManagementView {
dropdown.set_button_variant(ButtonVariant::Secondary);
}
fn update_harness_dropdown(&mut self, ctx: &mut ViewContext<Self>) {
let items = Self::build_harness_dropdown_items(ctx);
self.harness_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_rich_items(items, ctx);
});
}
/// Since the valid set of environments depends on what tasks we have loaded in,
/// we use this function to update the available options depending on the most recent
/// set of tasks.
@@ -771,7 +785,7 @@ impl AgentManagementView {
self.environment_dropdown.update(ctx, |dropdown, ctx| {
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetEnvironmentFilter(EnvironmentFilter::All),
),
),
@@ -779,7 +793,7 @@ impl AgentManagementView {
items.push(MenuItem::Item(
MenuItemFields::new("None").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetEnvironmentFilter(
EnvironmentFilter::NoEnvironment,
),
@@ -793,7 +807,7 @@ impl AgentManagementView {
for (environment_id, environment_name) in sorted_envs {
items.push(MenuItem::Item(
MenuItemFields::new(environment_name).with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetEnvironmentFilter(
EnvironmentFilter::Specific(environment_id),
),
@@ -818,7 +832,7 @@ impl AgentManagementView {
self.creator_dropdown.update(ctx, |dropdown, ctx| {
let mut items = vec![MenuItem::Item(
MenuItemFields::new("All").with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatorFilter(CreatorFilter::All),
),
),
@@ -826,7 +840,7 @@ impl AgentManagementView {
for (name, uid) in creators {
items.push(MenuItem::Item(
MenuItemFields::new(&name).with_on_select_action(
DropdownAction::SelectActionAndClose(
DropdownAction::select_action_and_close(
AgentManagementViewAction::SetCreatorFilter(CreatorFilter::Specific {
name,
uid,
@@ -938,46 +952,34 @@ impl AgentManagementView {
let model = AgentConversationsModel::as_ref(ctx);
let search_query = self.search_query.trim().to_lowercase();
let cards: Vec<CardData> = model
.get_tasks_and_conversations(&self.filters, ctx)
.filter(|t| {
.get_entries(&self.filters, ctx)
.into_iter()
.filter(|entry| {
if search_query.is_empty() {
return true;
}
match_indices_case_insensitive(&t.title(ctx), &search_query).is_some()
match_indices_case_insensitive(&entry.display.title, &search_query).is_some()
})
.map(|t| {
let item_id = match t {
ConversationOrTask::Task(task) => ManagementCardItemId::Task(task.task_id),
ConversationOrTask::Conversation(conversation) => {
ManagementCardItemId::Conversation(conversation.nav_data.id)
}
};
let artifacts = t.artifacts(ctx);
let copy_link_url = t.session_or_conversation_link(ctx);
let mut config = match t {
ConversationOrTask::Task(task) => ActionButtonsConfig::for_task(
task.task_id,
&t.display_status(ctx),
None, // Don't show open button in card hover
copy_link_url,
),
ConversationOrTask::Conversation(conversation) => {
ActionButtonsConfig::for_conversation(
conversation.nav_data.id,
None, // Don't show open button in card hover
copy_link_url,
.map(|entry| {
let item_id = entry.id;
let copy_link_url = entry
.capabilities
.can_copy_link
.then(|| {
AgentConversationsModel::resolve_copy_link(
AgentConversationNavigationSubject::Entry(entry.id),
ctx,
)
}
};
// Show info button in card hover for ViewDetails if feature flag enabled
})
.flatten();
let mut config = Self::action_buttons_config_for_entry(&entry, None, copy_link_url);
if FeatureFlag::AgentManagementDetailsView.is_enabled() {
config.view_details_item_id = Some(item_id.clone());
config.view_details_item_id = Some(item_id);
}
CardData {
item_id,
artifacts,
artifacts: entry.display.artifacts,
action_buttons_config: config,
}
})
@@ -1026,11 +1028,8 @@ impl AgentManagementView {
} else {
None
};
let action_buttons_view = self.create_action_buttons_view(
card.item_id.clone(),
card.action_buttons_config,
ctx,
);
let action_buttons_view =
self.create_action_buttons_view(card.item_id, card.action_buttons_config, ctx);
new_items.push(CardState {
hover_state: MouseStateHandle::default(),
@@ -1074,6 +1073,29 @@ impl AgentManagementView {
view
}
fn action_buttons_config_for_entry(
entry: &AgentConversationEntry,
open_action: Option<WorkspaceAction>,
copy_link_url: Option<String>,
) -> ActionButtonsConfig {
if let Some(task_id) = entry.identity.ambient_agent_task_id {
ActionButtonsConfig::for_task(
task_id,
&entry.display.status,
open_action,
copy_link_url,
)
} else if let Some(conversation_id) = entry.identity.local_conversation_id {
ActionButtonsConfig::for_conversation(conversation_id, open_action, copy_link_url)
} else {
ActionButtonsConfig {
open_action,
copy_link_url,
..Default::default()
}
}
}
fn handle_action_buttons_event(
&mut self,
item_id: &ManagementCardItemId,
@@ -1109,6 +1131,7 @@ impl AgentManagementView {
summarize_after_fork: false,
summarization_prompt: None,
initial_prompt: None,
initial_attachments: vec![],
destination: ForkedConversationDestination::NewTab,
});
}
@@ -1122,7 +1145,7 @@ impl AgentManagementView {
);
self.update_details_panel_for_item(item_id, ctx);
self.selected_item_id = Some(item_id.clone());
self.selected_item_id = Some(*item_id);
ctx.notify();
}
AgentDetailsButtonEvent::CopyLink { link } => {
@@ -1136,7 +1159,7 @@ impl AgentManagementView {
ctx
);
}
ManagementCardItemId::Task(task_id) => {
ManagementCardItemId::AmbientRun(task_id) => {
send_telemetry_from_ctx!(
AgentManagementTelemetryEvent::SessionLinkCopied {
task_id: task_id.to_string(),
@@ -1237,13 +1260,9 @@ impl AgentManagementView {
self.refresh_details_panel_if_needed(ctx);
self.get_tasks_from_model(ctx);
}
AgentConversationsModelEvent::ConversationUpdated => {
self.get_tasks_from_model(ctx);
self.refresh_details_panel_if_needed(ctx);
ctx.notify();
AgentConversationsModelEvent::ConversationUpdated { kind } => {
self.handle_conversation_updated(*kind, ctx);
}
// TaskManuallyOpened is handled by the conversation list view, not here.
AgentConversationsModelEvent::TaskManuallyOpened => {}
AgentConversationsModelEvent::ConversationArtifactsUpdated { conversation_id } => {
self.update_artifacts_for_conversation(*conversation_id, ctx);
self.refresh_details_panel_if_needed(ctx);
@@ -1253,11 +1272,48 @@ impl AgentManagementView {
/// Refresh the details panel if it's currently showing an item
fn refresh_details_panel_if_needed(&mut self, ctx: &mut ViewContext<Self>) {
if let Some(item_id) = self.selected_item_id.clone() {
if let Some(item_id) = self.selected_item_id {
self.update_details_panel_for_item(&item_id, ctx);
}
}
/// Decide how much work a `ConversationUpdated` event requires, based on its kind and the
/// active status filter:
/// * `Restored`: the underlying status didn't change, so the visible cards don't change
/// either. Just refresh the details panel.
/// * `MetadataChanged`: rebuild the cards so metadata-derived actions update.
/// * `TitleChanged`: rebuild the cards so filtering and titles update.
/// * `StatusSet` that crosses the active status filter: rebuild the
/// card list via `get_tasks_from_model`.
/// * `StatusSet` that doesn't cross the active filter (or `All` is active):
/// just refresh the details panel re-render so the status icon picks up the new value.
fn handle_conversation_updated(
&mut self,
kind: ConversationUpdateKind,
ctx: &mut ViewContext<Self>,
) {
match kind {
ConversationUpdateKind::Restored => {}
ConversationUpdateKind::MetadataChanged => self.get_tasks_from_model(ctx),
ConversationUpdateKind::TitleChanged => self.get_tasks_from_model(ctx),
ConversationUpdateKind::StatusSet {
prev_filter,
new_filter,
} => {
if self
.filters
.status
.is_membership_crossed(prev_filter, new_filter)
{
self.get_tasks_from_model(ctx);
} else {
ctx.notify();
}
}
}
self.refresh_details_panel_if_needed(ctx);
}
/// Update the details panel with fresh data for the given item.
fn update_details_panel_for_item(
&mut self,
@@ -1265,75 +1321,28 @@ impl AgentManagementView {
ctx: &mut ViewContext<Self>,
) {
let model = AgentConversationsModel::as_ref(ctx);
let data = match item_id {
ManagementCardItemId::Task(task_id) => {
let Some(task_wrapper) = model.get_task(task_id) else {
return;
};
// Agent management view should always open in a new tab
let open_action =
task_wrapper.get_open_action(Some(RestoreConversationLayout::NewTab), ctx);
let copy_link_url = task_wrapper.session_or_conversation_link(ctx);
let Some(task) = model.get_task_data(task_id) else {
return;
};
ConversationDetailsData::from_task(&task, open_action, copy_link_url, ctx)
}
ManagementCardItemId::Conversation(conversation_id) => {
let Some(conversation) = model.get_conversation(conversation_id) else {
return;
};
// Agent management view should always open in a new tab
let open_action =
conversation.get_open_action(Some(RestoreConversationLayout::NewTab), ctx);
let history_model = BlocklistAIHistoryModel::as_ref(ctx);
let ai_conversation = conversation
.navigation_data()
.and_then(|nav| history_model.conversation(&nav.id));
let server_conv_id = ai_conversation
.and_then(|c| c.server_conversation_token())
.map(|t| t.as_str().to_string())
.or_else(|| {
conversation
.navigation_data()
.and_then(|nav| history_model.get_conversation_metadata(&nav.id))
.and_then(|m| m.server_conversation_token.as_ref())
.map(|t| t.as_str().to_string())
});
let artifacts = ai_conversation
.map(|c| c.artifacts().to_vec())
.unwrap_or_default();
let status = Some(conversation.status(ctx));
let navigation_data = conversation.navigation_data();
let copy_link_url = conversation.session_or_conversation_link(ctx);
// Prefer server-reported harness when available; otherwise treat as a pure
// local conversation (always Warp Agent).
let harness = navigation_data
.and_then(|nav| history_model.get_server_conversation_metadata(&nav.id))
.map(|m| Harness::from(m.harness))
.or(Some(Harness::Oz));
ConversationDetailsData::from_conversation_metadata(
*conversation_id,
conversation.title(ctx),
conversation.creator_name(ctx),
conversation.created_at().with_timezone(&chrono::Local),
navigation_data.and_then(|n| n.initial_working_directory.clone()),
conversation.request_usage(ctx),
server_conv_id,
artifacts,
open_action,
status,
navigation_data.and_then(|n| n.initial_query.clone()),
copy_link_url,
harness,
)
}
let Some(entry) = model.get_entry_by_id(item_id, ctx) else {
return;
};
let open_action = AgentConversationsModel::resolve_open_action(
AgentConversationNavigationSubject::Entry(*item_id),
Some(RestoreConversationLayout::NewTab),
ctx,
);
let copy_link_url = AgentConversationsModel::resolve_copy_link(
AgentConversationNavigationSubject::Entry(*item_id),
ctx,
);
let task = entry
.identity
.ambient_agent_task_id
.and_then(|task_id| model.get_task_data(&task_id));
let data = ConversationDetailsData::from_agent_conversation_entry(
&entry,
task.as_ref(),
open_action,
copy_link_url,
);
self.details_panel.update(ctx, |p, ctx| {
p.set_conversation_details(data, ctx);
@@ -1347,19 +1356,14 @@ impl AgentManagementView {
ctx: &mut ViewContext<Self>,
) {
let model = AgentConversationsModel::as_ref(ctx);
let Some(card_data) = model.get_conversation(&conversation_id) else {
return;
};
let artifacts = card_data.artifacts(ctx);
// Find the index of the card for this conversation
let Some(index) = self
.items
.iter()
.position(|card| card.item_id == ManagementCardItemId::Conversation(conversation_id))
else {
let Some((index, entry)) = self.items.iter().enumerate().find_map(|(index, card)| {
let entry = model.get_entry_by_id(&card.item_id, ctx)?;
(entry.identity.local_conversation_id == Some(conversation_id))
.then_some((index, entry))
}) else {
return;
};
let artifacts = entry.display.artifacts;
// Update the artifact buttons for this card
if should_show_artifacts(&artifacts) {
@@ -1497,7 +1501,7 @@ impl AgentManagementView {
fn render_session_status_label(
appearance: &Appearance,
mouse_state: MouseStateHandle,
session_status: SessionStatus,
session_status: &SessionStatus,
) -> Box<dyn Element> {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
@@ -1604,21 +1608,17 @@ impl AgentManagementView {
};
let model = AgentConversationsModel::as_ref(app);
let card_data = match &card_state.item_id {
ManagementCardItemId::Task(task_id) => model.get_task(task_id),
ManagementCardItemId::Conversation(conv_id) => model.get_conversation(conv_id),
};
let Some(card_data) = card_data else {
let Some(entry) = model.get_entry_by_id(&card_state.item_id, app) else {
return Empty::new().finish();
};
self.render_card(card_state, &card_data, appearance, app)
self.render_card(card_state, &entry, appearance, app)
}
fn render_card(
&self,
card_state: &CardState,
card_data: &ConversationOrTask,
entry: &AgentConversationEntry,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
@@ -1642,10 +1642,8 @@ impl AgentManagementView {
let card_hoverable = Hoverable::new(card_state.hover_state.clone(), move |mouse_state| {
let mut card_content = Flex::column()
.with_spacing(CARD_ROW_SPACING)
.with_child(Self::render_header_row(
card_state, card_data, appearance, app,
))
.with_child(Self::render_metadata_row(card_data, appearance, app));
.with_child(Self::render_header_row(card_state, entry, appearance))
.with_child(Self::render_metadata_row(entry, appearance, app));
// Add artifacts row if there is a buttons view
if let Some(buttons_element) = artifact_buttons_element {
@@ -1706,18 +1704,12 @@ impl AgentManagementView {
})
.with_defer_events_to_children();
// Add click handler to open session if available
let item_id = card_state.item_id.clone();
let card_hoverable = if card_data
.get_open_action(Some(RestoreConversationLayout::NewTab), app)
.is_some()
{
let item_id = card_state.item_id;
let card_hoverable = if entry.capabilities.can_open {
card_hoverable
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(AgentManagementViewAction::OpenSession {
item_id: item_id.clone(),
});
ctx.dispatch_typed_action(AgentManagementViewAction::OpenSession { item_id });
})
} else {
card_hoverable
@@ -1728,29 +1720,30 @@ impl AgentManagementView {
fn render_header_row(
card_state: &CardState,
card_data: &ConversationOrTask,
entry: &AgentConversationEntry,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
let font_size = appearance.ui_font_size();
let title = card_data.title(app);
let title_text = Text::new_inline(title, font_family, font_size)
let title_text = Text::new_inline(entry.display.title.clone(), font_family, font_size)
.with_color(theme.active_ui_text_color().into());
let status_icon =
render_status_element(&card_data.display_status(app), STATUS_ICON_SIZE, appearance);
// Build the time and avatar elements
let last_updated = card_data.last_updated();
let time_str = format_approx_duration_from_now_utc(last_updated);
let status_icon = render_icon_with_status(
agent_conversation_entry_icon_variant(entry),
CARD_AGENT_ICON_SIZE,
0.,
theme,
internal_colors::fg_overlay_1(theme),
);
let time_str = format_approx_duration_from_now_utc(entry.display.last_updated);
let time_text = Text::new_inline(time_str, font_family, font_size)
.with_color(theme.nonactive_ui_text_color().into());
let creator_name = card_data
.creator_name(app)
let creator_name = entry
.display
.creator
.name
.clone()
.unwrap_or_else(|| "Unknown".to_string());
let avatar = Self::render_avatar_with_tooltip(
&creator_name,
@@ -1763,12 +1756,11 @@ impl AgentManagementView {
.with_spacing(2.)
.with_child(Container::new(status_icon).with_margin_right(4.).finish())
.with_child(Expanded::new(1., title_text.finish()).finish());
let mut time_and_avatar = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_spacing(4.);
if let Some(session_status) = card_data.get_session_status() {
if let Some(session_status) = &entry.display.session_status {
time_and_avatar.add_child(Self::render_session_status_label(
appearance,
card_state.session_status_hover_state.clone(),
@@ -1792,45 +1784,56 @@ impl AgentManagementView {
}
fn render_metadata_row(
card_data: &ConversationOrTask,
entry: &AgentConversationEntry,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let theme = appearance.theme();
let font_family = appearance.ui_font_family();
let font_size = appearance.ui_font_size();
// Build metadata parts conditionally
let mut metadata_parts = Vec::new();
if let Some(source) = card_data.source() {
if let Some(source) = &entry.display.source {
metadata_parts.push(format!("Source: {}", source.display_name()));
}
if FeatureFlag::AgentHarness.is_enabled() {
if let Some(harness) = card_data.harness() {
let availability = HarnessAvailabilityModel::as_ref(app);
if availability.should_show_harness_selector() {
if let Some(harness) = entry.display.harness {
metadata_parts.push(format!(
"Harness: {}",
harness_display::display_name(harness)
availability.display_name_for(harness)
));
}
}
if let Some(run_time) = card_data.run_time() {
if let Some(executor) = &entry.display.executor {
let same_as_creator =
executor.uid.is_some() && executor.uid == entry.display.creator.uid;
if !same_as_creator {
if let Some(name) = executor.name.as_deref().or(executor.uid.as_deref()) {
let label = if executor
.principal_type
.is_some_and(|pt| pt.is_service_account())
{
"Agent"
} else {
"Executor"
};
metadata_parts.push(format!("{label}: {name}"));
}
}
}
if let Some(run_time) = &entry.display.run_time {
metadata_parts.push(format!("Run time: {run_time}"));
}
if let Some(usage) = card_data.display_request_usage(app) {
if let Some(usage) = entry.display.request_usage.map(format_credits) {
metadata_parts.push(format!("Credits used: {usage}"));
}
if let Some(tokens) = card_data.display_total_tokens(app) {
metadata_parts.push(format!("Tokens: {tokens}"));
}
let metadata_text = metadata_parts.join("");
Text::new(metadata_text, font_family, font_size)
Text::new(metadata_parts.join(""), font_family, font_size)
.with_color(theme.nonactive_ui_text_color().into())
.finish()
}
@@ -1947,7 +1950,7 @@ impl AgentManagementView {
.with_child(ChildView::new(&self.created_on_dropdown).finish())
.with_child(ChildView::new(&self.artifact_dropdown).finish());
if FeatureFlag::AgentHarness.is_enabled() {
if HarnessAvailabilityModel::as_ref(app).should_show_harness_selector() {
filters_wrap.add_child(ChildView::new(&self.harness_dropdown).finish());
}
@@ -2356,17 +2359,11 @@ impl TypedActionView for AgentManagementView {
ctx.notify();
}
AgentManagementViewAction::OpenSession { item_id } => {
let model = AgentConversationsModel::as_ref(ctx);
let card_data = match item_id {
ManagementCardItemId::Task(task_id) => model.get_task(task_id),
ManagementCardItemId::Conversation(conv_id) => model.get_conversation(conv_id),
};
let Some(card_data) = card_data else {
return;
};
let Some(action) =
card_data.get_open_action(Some(RestoreConversationLayout::NewTab), ctx)
else {
let Some(action) = AgentConversationsModel::resolve_open_action(
AgentConversationNavigationSubject::Entry(*item_id),
Some(RestoreConversationLayout::NewTab),
ctx,
) else {
return;
};
@@ -2380,7 +2377,7 @@ impl TypedActionView for AgentManagementView {
ctx
);
}
ManagementCardItemId::Task(task_id) => {
ManagementCardItemId::AmbientRun(task_id) => {
send_telemetry_from_ctx!(
AgentManagementTelemetryEvent::CloudRunOpened {
task_id: task_id.to_string(),