Skip to content
Draft
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
11 changes: 11 additions & 0 deletions app/src/ai/agent_conversations_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ pub enum CreatedOnFilter {
LastWeek,
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub enum SubagentsFilter {
#[default]
All,
Hide,
}

#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub enum EnvironmentFilter {
#[default]
Expand Down Expand Up @@ -244,6 +251,8 @@ pub struct AgentManagementFilters {
pub creator: CreatorFilter,
pub artifact: ArtifactFilter,
#[serde(default)]
pub subagents: SubagentsFilter,
#[serde(default)]
pub environment: EnvironmentFilter,
#[serde(default)]
pub harness: HarnessFilter,
Expand All @@ -256,6 +265,7 @@ impl AgentManagementFilters {
self.created_on = CreatedOnFilter::default();
self.creator = CreatorFilter::default();
self.artifact = ArtifactFilter::default();
self.subagents = SubagentsFilter::default();
self.environment = EnvironmentFilter::default();
self.harness = HarnessFilter::default();
}
Expand All @@ -266,6 +276,7 @@ impl AgentManagementFilters {
|| self.created_on != CreatedOnFilter::default()
|| self.creator != CreatorFilter::default() && self.owners != OwnerFilter::PersonalOnly
|| self.artifact != ArtifactFilter::default()
|| self.subagents != SubagentsFilter::default()
|| self.environment != EnvironmentFilter::default()
|| self.harness != HarnessFilter::default()
}
Expand Down
19 changes: 19 additions & 0 deletions app/src/ai/agent_conversations_model/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ pub struct AgentConversationEntry {
pub id: AgentConversationEntryId,
pub identity: AgentConversationIdentity,
pub provenance: AgentConversationProvenance,
pub is_subagent: bool,
pub display: AgentConversationDisplayData,
pub backing: AgentConversationBackingData,
pub capabilities: AgentConversationCapabilities,
Expand Down Expand Up @@ -177,6 +178,7 @@ impl AgentConversationEntry {
&& self.matches_artifact(&filters.artifact)
&& self.matches_environment(&filters.environment)
&& self.matches_harness(&filters.harness)
&& self.matches_subagent(&filters.subagents)
}

fn matches_owner_and_creator(
Expand Down Expand Up @@ -262,6 +264,13 @@ impl AgentConversationEntry {
}
}

fn matches_subagent(&self, subagents_filter: &super::SubagentsFilter) -> bool {
match subagents_filter {
super::SubagentsFilter::All => true,
super::SubagentsFilter::Hide => !self.is_subagent,
}
}

pub fn has_open_action(
&self,
restore_layout: Option<RestoreConversationLayout>,
Expand Down Expand Up @@ -442,6 +451,12 @@ pub(super) fn entry_for_task(
let can_copy_link = task.has_active_execution()
&& task.active_run_execution().session_link.is_some()
|| server_conversation_token.is_some();
let is_subagent = task.parent_run_id.is_some()
|| local_conversation_id.is_some_and(|id| {
history_model
.conversation(&id)
.is_some_and(|conversation| conversation.is_child_agent_conversation())
});

AgentConversationEntry {
id: AgentConversationEntryId::AmbientRun(task.task_id),
Expand All @@ -452,6 +467,7 @@ pub(super) fn entry_for_task(
session_id: task_session_id(task),
},
provenance: AgentConversationProvenance::AmbientRun,
is_subagent,
display: AgentConversationDisplayData {
title: task.title.clone(),
initial_query: Some(task.prompt.clone()),
Expand Down Expand Up @@ -573,6 +589,9 @@ fn entry_for_conversation_parts(
session_id: None,
},
provenance,
is_subagent: history_model
.conversation(&conversation_id)
.is_some_and(|conversation| conversation.is_child_agent_conversation()),
display: AgentConversationDisplayData {
title: conversation_title(&metadata, history_model),
initial_query: metadata.nav_data.initial_query.clone(),
Expand Down
89 changes: 87 additions & 2 deletions app/src/ai/agent_conversations_model_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ use super::{
record_earliest_rtc_task_refresh_timestamp, AgentConversationsModel,
AgentConversationsModelEvent, AgentManagementFilters, AgentRunDisplayStatus, ArtifactFilter,
ConversationMetadata, ConversationUpdateKind, EnvironmentFilter, HarnessFilter, OwnerFilter,
RtcTaskRefreshThrottleState, StatusFilter, TaskFetchState, MAX_PERSONAL_TASKS, MAX_TEAM_TASKS,
RtcTaskRefreshThrottleState, StatusFilter, SubagentsFilter, TaskFetchState, MAX_PERSONAL_TASKS,
MAX_TEAM_TASKS,
};
use crate::ai::ambient_agents::task::HarnessConfig;
use crate::workspace::WorkspaceAction;
Expand Down Expand Up @@ -668,6 +669,26 @@ fn all_owner_filters() -> AgentManagementFilters {
}
}

fn default_conversation_data() -> AgentConversationData {
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,
orchestration_harness_type: None,
parent_conversation_id: None,
is_remote_child: false,
root_task_is_optimistic: None,
run_id: None,
autoexecute_override: None,
last_event_sequence: None,
pinned: false,
}
}

fn add_entry_projection_test_models(app: &mut App) {
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.add_singleton_model(|_| BlocklistAIHistoryModel::new(vec![], &[]));
Expand Down Expand Up @@ -779,6 +800,69 @@ fn test_get_entries_includes_local_only_entry() {
});
}

#[test]
fn test_get_entries_can_hide_local_subagent_conversation() {
App::test((), |mut app| async move {
add_entry_projection_test_models(&mut app);

let conversation_id = AIConversationId::new();
let terminal_view_id = EntityId::new();
let mut conversation_data = default_conversation_data();
conversation_data.parent_agent_id = Some("parent-run-id".to_string());
let child_conversation =
create_restored_conversation(conversation_id, "child-task", conversation_data);

BlocklistAIHistoryModel::handle(&app).update(&mut app, |model, ctx| {
model.restore_conversations(terminal_view_id, vec![child_conversation], ctx);
});

let mut model = create_test_model();
model.conversations.insert(
conversation_id,
create_test_conversation_metadata(conversation_id, "Child conversation"),
);

app.update(|ctx| {
let all_entries = model.get_entries(&all_owner_filters(), ctx);
assert_eq!(all_entries.len(), 1);
assert!(all_entries[0].is_subagent);

let hidden_entries = model.get_entries(
&AgentManagementFilters {
subagents: SubagentsFilter::Hide,
..all_owner_filters()
},
ctx,
);
assert!(hidden_entries.is_empty());
});
});
}

#[test]
fn test_get_entries_can_hide_task_subagent() {
App::test((), |mut app| async move {
add_entry_projection_test_models(&mut app);

let now = Utc::now();
let mut model = create_test_model();
let mut task = create_test_task(&make_uuid(8150), "user-a", now);
task.parent_run_id = Some("parent-run-id".to_string());
model.tasks.insert(task.task_id, task);

app.update(|ctx| {
let hidden_entries = model.get_entries(
&AgentManagementFilters {
subagents: SubagentsFilter::Hide,
..all_owner_filters()
},
ctx,
);
assert!(hidden_entries.is_empty());
});
});
}

#[test]
fn test_get_entries_includes_cloud_metadata_only_entry() {
App::test((), |mut app| async move {
Expand Down Expand Up @@ -2251,7 +2335,7 @@ fn test_get_or_async_fetch_task_data_skips_within_transient_cooldown() {

#[test]
fn test_agent_management_filters_serde_backwards_compat() {
// Persisted state from older clients has no `harness` key → deserializes to All.
// Persisted state from older clients has no newer filter keys -> deserializes to defaults.
let legacy = r#"{
"owners": "PersonalOnly",
"status": "All",
Expand All @@ -2263,6 +2347,7 @@ fn test_agent_management_filters_serde_backwards_compat() {
let decoded: AgentManagementFilters =
serde_json::from_str(legacy).expect("legacy payload without harness must deserialize");
assert_eq!(decoded.harness, HarnessFilter::All);
assert_eq!(decoded.subagents, SubagentsFilter::All);

// Round trip a Specific(Claude) value.
let original = AgentManagementFilters {
Expand Down
51 changes: 49 additions & 2 deletions app/src/ai/agent_management/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::ai::agent_conversations_model::{
AgentConversationEntry, AgentConversationEntryId, AgentConversationNavigationSubject,
AgentConversationsModel, AgentConversationsModelEvent, AgentManagementFilters, ArtifactFilter,
ConversationUpdateKind, CreatedOnFilter, CreatorFilter, EnvironmentFilter, HarnessFilter,
OwnerFilter, SessionStatus, SourceFilter, StatusFilter,
OwnerFilter, SessionStatus, SourceFilter, StatusFilter, SubagentsFilter,
};
use crate::ai::agent_management::agent_type_selector::{
AgentType, AgentTypeSelector, AgentTypeSelectorEvent,
Expand Down Expand Up @@ -176,6 +176,7 @@ pub struct AgentManagementView {
source_dropdown: ViewHandle<Dropdown<AgentManagementViewAction>>,
created_on_dropdown: ViewHandle<Dropdown<AgentManagementViewAction>>,
artifact_dropdown: ViewHandle<Dropdown<AgentManagementViewAction>>,
subagents_dropdown: ViewHandle<Dropdown<AgentManagementViewAction>>,
harness_dropdown: ViewHandle<Dropdown<AgentManagementViewAction>>,
environment_dropdown: ViewHandle<FilterableDropdown<AgentManagementViewAction>>,
creator_dropdown: ViewHandle<FilterableDropdown<AgentManagementViewAction>>,
Expand Down Expand Up @@ -265,6 +266,7 @@ impl AgentManagementView {
let source_dropdown = ctx.add_typed_action_view(Self::create_source_dropdown);
let created_on_dropdown = ctx.add_typed_action_view(Self::create_created_on_dropdown);
let artifact_dropdown = ctx.add_typed_action_view(Self::create_artifact_dropdown);
let subagents_dropdown = ctx.add_typed_action_view(Self::create_subagents_dropdown);
let harness_dropdown = ctx.add_typed_action_view(Self::create_harness_dropdown);
let environment_dropdown = ctx.add_typed_action_view(Self::create_environment_dropdown);
let creator_dropdown = ctx.add_typed_action_view(Self::create_creator_dropdown);
Expand Down Expand Up @@ -359,6 +361,7 @@ impl AgentManagementView {
source_dropdown,
created_on_dropdown,
artifact_dropdown,
subagents_dropdown,
harness_dropdown,
environment_dropdown,
creator_dropdown,
Expand Down Expand Up @@ -448,6 +451,13 @@ impl AgentManagementView {
);
});

self.subagents_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_action(
AgentManagementViewAction::SetSubagentsFilter(self.filters.subagents),
ctx,
);
});

self.harness_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_action(
AgentManagementViewAction::SetHarnessFilter(self.filters.harness),
Expand Down Expand Up @@ -661,6 +671,30 @@ impl AgentManagementView {
dropdown
}

fn create_subagents_dropdown(
ctx: &mut ViewContext<Dropdown<AgentManagementViewAction>>,
) -> Dropdown<AgentManagementViewAction> {
let mut dropdown = Dropdown::new(ctx);
Self::setup_filter_menu(&mut dropdown, "Subagents", ctx);

let items = vec![
MenuItem::Item(MenuItemFields::new("Show").with_on_select_action(
DropdownAction::SelectActionAndClose(
AgentManagementViewAction::SetSubagentsFilter(SubagentsFilter::All),
),
)),
MenuItem::Item(MenuItemFields::new("Hide").with_on_select_action(
DropdownAction::SelectActionAndClose(
AgentManagementViewAction::SetSubagentsFilter(SubagentsFilter::Hide),
),
)),
];

dropdown.set_rich_items(items, ctx);
dropdown.set_selected_by_index(0, ctx);
dropdown
}

fn create_harness_dropdown(
ctx: &mut ViewContext<Dropdown<AgentManagementViewAction>>,
) -> Dropdown<AgentManagementViewAction> {
Expand Down Expand Up @@ -926,6 +960,9 @@ impl AgentManagementView {
self.artifact_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_index(0, ctx);
});
self.subagents_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_index(0, ctx);
});
self.harness_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_index(0, ctx);
});
Expand Down Expand Up @@ -1940,7 +1977,8 @@ impl AgentManagementView {
.with_child(ChildView::new(&self.status_dropdown).finish())
.with_child(ChildView::new(&self.source_dropdown).finish())
.with_child(ChildView::new(&self.created_on_dropdown).finish())
.with_child(ChildView::new(&self.artifact_dropdown).finish());
.with_child(ChildView::new(&self.artifact_dropdown).finish())
.with_child(ChildView::new(&self.subagents_dropdown).finish());

if HarnessAvailabilityModel::as_ref(app).should_show_harness_selector() {
filters_wrap.add_child(ChildView::new(&self.harness_dropdown).finish());
Expand Down Expand Up @@ -2212,6 +2250,7 @@ pub enum AgentManagementViewAction {
SetSourceFilter(SourceFilter),
SetCreatedOnFilter(CreatedOnFilter),
SetArtifactFilter(ArtifactFilter),
SetSubagentsFilter(SubagentsFilter),
SetEnvironmentFilter(EnvironmentFilter),
SetCreatorFilter(CreatorFilter),
SetHarnessFilter(HarnessFilter),
Expand Down Expand Up @@ -2278,6 +2317,11 @@ impl TypedActionView for AgentManagementView {
self.get_tasks_from_model(ctx);
ctx.dispatch_global_action("workspace:save_app", ());
}
AgentManagementViewAction::SetSubagentsFilter(filter) => {
self.filters.subagents = *filter;
self.get_tasks_from_model(ctx);
ctx.dispatch_global_action("workspace:save_app", ());
}
AgentManagementViewAction::SetEnvironmentFilter(filter) => {
self.filters.environment = filter.clone();
self.on_filter_changed(ctx);
Expand Down Expand Up @@ -2318,6 +2362,9 @@ impl TypedActionView for AgentManagementView {
self.artifact_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_index(0, ctx);
});
self.subagents_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_index(0, ctx);
});
self.harness_dropdown.update(ctx, |dropdown, ctx| {
dropdown.set_selected_by_index(0, ctx);
});
Expand Down
1 change: 1 addition & 0 deletions app/src/ui_components/agent_icon_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ fn non_ambient_entry_uses_display_harness() {
session_id: None,
},
provenance: AgentConversationProvenance::CloudSyncedConversation,
is_subagent: false,
display: AgentConversationDisplayData {
title: "Codex conversation".to_string(),
initial_query: None,
Expand Down
Loading