diff --git a/app/src/ai/agent_conversations_model.rs b/app/src/ai/agent_conversations_model.rs index aac762b52bc..91e4ac96a72 100644 --- a/app/src/ai/agent_conversations_model.rs +++ b/app/src/ai/agent_conversations_model.rs @@ -920,6 +920,9 @@ impl AgentConversationsModel { updated_after: Some(updated_after), ..Default::default() }, + // TODO: Multi-team UI needs a team component in the cache or equivalent + // scope-aware filtering before this request can supply team scope. + None, ) .await } @@ -992,6 +995,9 @@ impl AgentConversationsModel { creator_uid: Some(creator_uid), ..Default::default() }, + // TODO: Multi-team UI needs a team component in the cache or equivalent + // scope-aware filtering before this request can supply team scope. + None, ); let conversation_metadata_future = ai_client.list_ai_conversation_metadata(None); @@ -1221,7 +1227,13 @@ impl AgentConversationsModel { let ai_client = ai_client.clone(); async move { ai_client - .list_ambient_agent_tasks(100, TaskListFilter::default()) + .list_ambient_agent_tasks( + 100, + TaskListFilter::default(), + // TODO: Multi-team UI needs a team component in the cache or equivalent + // scope-aware filtering before this request can supply team scope. + None, + ) .await } }, @@ -2035,7 +2047,13 @@ impl AgentConversationsModel { let task_filter = task_filter.clone(); async move { ai_client - .list_ambient_agent_tasks(INITIAL_TASK_AMOUNT, task_filter) + .list_ambient_agent_tasks( + INITIAL_TASK_AMOUNT, + task_filter, + // TODO: Multi-team UI needs a team component in the cache or equivalent + // scope-aware filtering before this request can supply team scope. + None, + ) .await } }, diff --git a/app/src/ai/agent_sdk/ambient.rs b/app/src/ai/agent_sdk/ambient.rs index 44b48a8ce26..5de0bcb608c 100644 --- a/app/src/ai/agent_sdk/ambient.rs +++ b/app/src/ai/agent_sdk/ambient.rs @@ -46,6 +46,7 @@ use crate::server::server_api::ai::{ ListAgentMessagesRequest, ReadAgentMessageResponse, RunSortBy, RunSortOrder, SendAgentMessageRequest, SendAgentMessageResponse, SpawnAgentRequest, TaskListFilter, }; +use crate::server::team_scope::RequestTeamScope; use crate::terminal::shared_session; use crate::util::time_format::format_approx_duration_from_now_utc; use crate::workspaces::user_workspaces::UserWorkspaces; @@ -78,7 +79,14 @@ pub fn list_ambient_agent_tasks( let json_output = args.json_output.clone(); let output_format = global_options.output_format; runner.update(ctx, |runner, ctx| { - runner.list_tasks(args.limit, filter, output_format, json_output, ctx) + runner.list_tasks( + args.team_selection, + args.limit, + filter, + output_format, + json_output, + ctx, + ) }) } @@ -195,6 +203,39 @@ fn sort_order_from_arg(arg: SortOrderArg) -> RunSortOrder { } } +fn request_team_scope_for_cli( + team_selection: &warp_cli::scope::TeamSelection, + ctx: &AppContext, +) -> anyhow::Result { + let cli_scope = UserWorkspaces::as_ref(ctx).team_scope_for_cli(team_selection)?; + Ok(RequestTeamScope::from_scope(&cli_scope)) +} + +enum ListTasksOutput { + Raw(serde_json::Value), + Tasks(Vec), +} + +async fn load_tasks_for_output( + ai_client: &dyn AIClient, + limit: i32, + filter: TaskListFilter, + request_team_scope: RequestTeamScope, + output_format: OutputFormat, + json_output: &JsonOutput, +) -> anyhow::Result { + if matches!(output_format, OutputFormat::Json) || json_output.force_json_output() { + let response = ai_client + .list_agent_runs_raw(limit, filter, Some(request_team_scope)) + .await?; + Ok(ListTasksOutput::Raw(response)) + } else { + let tasks = ai_client + .list_ambient_agent_tasks(limit, filter, Some(request_team_scope)) + .await?; + Ok(ListTasksOutput::Tasks(tasks)) + } +} /// Run a message-related CLI command. pub fn run_message( ctx: &mut AppContext, @@ -640,30 +681,56 @@ impl AmbientAgentRunner { fn list_tasks( &self, + team_selection: warp_cli::scope::TeamSelection, limit: i32, filter: TaskListFilter, output_format: OutputFormat, json_output: JsonOutput, ctx: &mut ModelContext, ) -> anyhow::Result<()> { - let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client(); - - let list_future = async move { - if matches!(output_format, OutputFormat::Json) || json_output.force_json_output() { - let response = ai_client.list_agent_runs_raw(limit, filter).await?; - super::output::print_raw_json(response, &json_output)?; - } else if matches!(output_format, OutputFormat::Ndjson) { - let tasks = ai_client.list_ambient_agent_tasks(limit, filter).await?; - for task in tasks { - super::output::write_json_line(&task, std::io::stdout())?; - } - } else { - let tasks = ai_client.list_ambient_agent_tasks(limit, filter).await?; - Self::print_tasks_table(&tasks); + let refresh_future = super::common::refresh_workspace_metadata(ctx); + ctx.spawn(refresh_future, move |runner, refresh_result, ctx| { + if let Err(err) = refresh_result { + super::report_fatal_error(err, ctx); + return; } - Ok(()) - }; - self.spawn_command(list_future, ctx); + let request_team_scope = match request_team_scope_for_cli(&team_selection, ctx) { + Ok(scope) => scope, + Err(err) => { + super::report_fatal_error(err, ctx); + return; + } + }; + let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client(); + let list_future = async move { + match load_tasks_for_output( + ai_client.as_ref(), + limit, + filter, + request_team_scope, + output_format, + &json_output, + ) + .await? + { + ListTasksOutput::Raw(response) => { + super::output::print_raw_json(response, &json_output)?; + } + ListTasksOutput::Tasks(tasks) + if matches!(output_format, OutputFormat::Ndjson) => + { + for task in tasks { + super::output::write_json_line(&task, std::io::stdout())?; + } + } + ListTasksOutput::Tasks(tasks) => { + Self::print_tasks_table(&tasks); + } + } + Ok(()) + }; + runner.spawn_command(list_future, ctx); + }); Ok(()) } diff --git a/app/src/ai/agent_sdk/ambient_tests.rs b/app/src/ai/agent_sdk/ambient_tests.rs index 5f864b9fab1..24d43032f7b 100644 --- a/app/src/ai/agent_sdk/ambient_tests.rs +++ b/app/src/ai/agent_sdk/ambient_tests.rs @@ -1,17 +1,36 @@ //! Unit tests for ambient agent CLI argument mapping and message helpers. +use std::sync::Arc; + use bytes::Bytes; use chrono::{TimeZone, Utc}; use warp_cli::SortOrderArg; -use warp_cli::json_filter::JsonOutput; +use warp_cli::json_filter::{JsonOutput, parse_jq_filter}; +use warp_cli::scope::TeamSelection; use warp_cli::task::{ ArtifactTypeArg, ExecutionLocationArg, ListTasksArgs, RunSortByArg, RunSourceArg, RunStateArg, }; use warp_server_client::HttpStatusError; +use warpui::App; use super::*; +use crate::auth::AuthStateProvider; +use crate::network::NetworkStatus; +use crate::server::ids::ServerId; use crate::server::server_api::ai::{ ArtifactType, ExecutionLocation, MockAIClient, RunSortBy, RunSortOrder, }; +use crate::server::server_api::team::{MockTeamClient, TeamClient}; +use crate::server::server_api::workspace::MockWorkspaceClient; +use crate::server::team_scope::RequestTeamScope; +use crate::settings::PrivacySettings; +use crate::workspaces::team::Team; +use crate::workspaces::team_tester::TeamTesterStatus; +use crate::workspaces::update_manager::TeamUpdateManager; +use crate::workspaces::user_workspaces::{ + TeamContextForOperation, TeamlessScopeForTest, UserWorkspaces, WorkspacesMetadataResponse, + WorkspacesMetadataWithPricing, +}; +use crate::workspaces::workspace::{Workspace, WorkspaceUid}; const TASK_ID: &str = "00000000-0000-0000-0000-000000000001"; const OTHER_TASK_ID: &str = "00000000-0000-0000-0000-000000000002"; @@ -19,6 +38,7 @@ const OTHER_TASK_ID: &str = "00000000-0000-0000-0000-000000000002"; /// A `ListTasksArgs` whose fields are all at their defaults. fn empty_args() -> ListTasksArgs { ListTasksArgs { + team_selection: TeamSelection { team: None }, limit: 10, state: vec![], source: None, @@ -42,6 +62,14 @@ fn empty_args() -> ListTasksArgs { } } +fn request_team_scope() -> RequestTeamScope { + RequestTeamScope::from_scope(&TeamlessScopeForTest) +} + +fn request_scope_for_team(team_uid: ServerId) -> RequestTeamScope { + RequestTeamScope::from_scope(&TeamContextForOperation::new_for_test(team_uid)) +} + #[test] fn empty_args_yields_default_filter() { let filter = filter_from_args(&empty_args()); @@ -125,6 +153,7 @@ fn every_field_maps_through() { let updated_after = Utc.with_ymd_and_hms(2026, 4, 3, 12, 30, 0).unwrap(); let args = ListTasksArgs { + team_selection: TeamSelection { team: None }, limit: 20, state: vec![RunStateArg::InProgress], source: Some(RunSourceArg::Api), @@ -172,6 +201,192 @@ fn every_field_maps_through() { assert_eq!(filter.cursor.as_deref(), Some("abcd==")); } +#[tokio::test] +async fn table_run_listing_supplies_cli_scope() { + let mut mock = MockAIClient::new(); + mock.expect_list_ambient_agent_tasks() + .withf(|limit, filter, request_team_scope| { + *limit == 10 + && filter.creator_uid.is_none() + && filter.states.is_none() + && request_team_scope.is_some() + }) + .times(1) + .returning(|_, _, _| Ok(Vec::new())); + mock.expect_list_agent_runs_raw().times(0); + + load_tasks_for_output( + &mock, + 10, + TaskListFilter::default(), + request_team_scope(), + OutputFormat::Text, + &JsonOutput::default(), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn ndjson_run_listing_supplies_cli_scope() { + let mut mock = MockAIClient::new(); + mock.expect_list_ambient_agent_tasks() + .withf(|limit, filter, request_team_scope| { + *limit == 10 + && filter.creator_uid.is_none() + && filter.states.is_none() + && request_team_scope.is_some() + }) + .times(1) + .returning(|_, _, _| Ok(Vec::new())); + mock.expect_list_agent_runs_raw().times(0); + + load_tasks_for_output( + &mock, + 10, + TaskListFilter::default(), + request_team_scope(), + OutputFormat::Ndjson, + &JsonOutput::default(), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn raw_json_run_listing_supplies_cli_scope() { + let mut mock = MockAIClient::new(); + mock.expect_list_agent_runs_raw() + .withf(|limit, filter, request_team_scope| { + *limit == 10 + && filter.creator_uid.is_none() + && filter.states.is_none() + && request_team_scope.is_some() + }) + .times(1) + .returning(|_, _, _| Ok(serde_json::json!({ "runs": [] }))); + mock.expect_list_ambient_agent_tasks().times(0); + + load_tasks_for_output( + &mock, + 10, + TaskListFilter::default(), + request_team_scope(), + OutputFormat::Json, + &JsonOutput::default(), + ) + .await + .unwrap(); +} + +#[tokio::test] +async fn jq_run_listing_supplies_selected_cli_scope_to_raw_request() { + let team_uid = ServerId::from(123); + let mut mock = MockAIClient::new(); + mock.expect_list_agent_runs_raw() + .withf(move |limit, filter, request_team_scope| { + *limit == 10 + && filter.creator_uid.is_none() + && filter.states.is_none() + && request_team_scope.and_then(RequestTeamScope::team_uid) == Some(team_uid) + }) + .times(1) + .returning(|_, _, _| Ok(serde_json::json!({ "runs": [] }))); + mock.expect_list_ambient_agent_tasks().times(0); + let json_output = JsonOutput { + filter: Some(parse_jq_filter(".runs").expect("jq filter compiles")), + }; + + load_tasks_for_output( + &mock, + 10, + TaskListFilter::default(), + request_scope_for_team(team_uid), + OutputFormat::Text, + &json_output, + ) + .await + .unwrap(); +} + +#[test] +fn run_list_scope_uses_team_loaded_by_workspace_refresh() { + App::test((), |mut app| async move { + let team_uid = ServerId::from(123); + let workspace_uid = WorkspaceUid::from(ServerId::from(456)); + let team = Team::from_local_cache( + team_uid, + "Selected Team".to_string(), + None, + None, + None, + None, + ); + let workspace = Workspace::from_local_cache( + workspace_uid, + "Selected Workspace".to_string(), + Some(vec![team]), + None, + ); + let mut team_client = MockTeamClient::new(); + team_client + .expect_workspaces_metadata() + .times(1) + .return_once(move || { + Ok(WorkspacesMetadataWithPricing { + metadata: WorkspacesMetadataResponse { + workspaces: vec![workspace], + joinable_teams: vec![], + experiments: None, + ai_credit_availability: None, + user_purchase_policy: None, + }, + pricing_info: None, + }) + }); + let team_client: Arc = Arc::new(team_client); + + app.add_singleton_model(|_| NetworkStatus::new()); + app.add_singleton_model(TeamTesterStatus::new); + app.add_singleton_model(PrivacySettings::mock); + app.add_singleton_model(|_| AuthStateProvider::new_for_test()); + app.add_singleton_model({ + let team_client = team_client.clone(); + move |ctx| { + UserWorkspaces::mock( + team_client, + Arc::new(MockWorkspaceClient::new()), + vec![], + ctx, + ) + } + }); + app.add_singleton_model(move |ctx| TeamUpdateManager::new(team_client, None, ctx)); + + assert!( + app.read(|ctx| UserWorkspaces::as_ref(ctx) + .team_from_uid(team_uid) + .is_none()), + "the selected team should not exist in the initial cache" + ); + + app.update(crate::ai::agent_sdk::common::refresh_workspace_metadata) + .await + .expect("workspace refresh succeeds"); + + app.read(|ctx| { + let scope = request_team_scope_for_cli( + &TeamSelection { + team: Some(Some(team_uid.to_string())), + }, + ctx, + ) + .expect("the selected team resolves from refreshed metadata"); + assert_eq!(scope.team_uid(), Some(team_uid)); + }); + }); +} + #[test] fn task_id_from_run_id_accepts_task_uuid() { let task_id = task_id_from_run_id(TASK_ID).expect("valid task id"); diff --git a/app/src/ai/agent_sdk/mod.rs b/app/src/ai/agent_sdk/mod.rs index eeadd8dff33..ded67d28f91 100644 --- a/app/src/ai/agent_sdk/mod.rs +++ b/app/src/ai/agent_sdk/mod.rs @@ -585,7 +585,7 @@ fn run_task( command: TaskCommand, ) -> anyhow::Result<()> { match command { - TaskCommand::List(args) => ambient::list_ambient_agent_tasks(ctx, global_options, args), + TaskCommand::List(args) => ambient::list_ambient_agent_tasks(ctx, global_options, *args), TaskCommand::Get(args) => { if args.conversation { if !FeatureFlag::ConversationApi.is_enabled() { diff --git a/app/src/ai/blocklist/orchestration_event_streamer.rs b/app/src/ai/blocklist/orchestration_event_streamer.rs index efc55e85e1d..a53b6d0024d 100644 --- a/app/src/ai/blocklist/orchestration_event_streamer.rs +++ b/app/src/ai/blocklist/orchestration_event_streamer.rs @@ -1358,7 +1358,7 @@ impl OrchestrationEventStreamer { ctx.spawn( async move { ai_client - .list_ambient_agent_tasks(VIEWER_MODE_SEED_FETCH_LIMIT, filter) + .list_ambient_agent_tasks(VIEWER_MODE_SEED_FETCH_LIMIT, filter, None) .await }, move |me, result, ctx| { diff --git a/app/src/pane_group/child_agent/restoration.rs b/app/src/pane_group/child_agent/restoration.rs index fc08a930dca..00f58ac53ea 100644 --- a/app/src/pane_group/child_agent/restoration.rs +++ b/app/src/pane_group/child_agent/restoration.rs @@ -204,7 +204,7 @@ impl PaneGroup { ctx.spawn( async move { ai_client - .list_ambient_agent_tasks(RESTORE_CHILD_SEED_FETCH_LIMIT, filter) + .list_ambient_agent_tasks(RESTORE_CHILD_SEED_FETCH_LIMIT, filter, None) .await }, move |me, result, ctx| { diff --git a/app/src/server/server_api/ai.rs b/app/src/server/server_api/ai.rs index 31eace9b15d..64dc46c55ae 100644 --- a/app/src/server/server_api/ai.rs +++ b/app/src/server/server_api/ai.rs @@ -7,7 +7,7 @@ use ai::index::full_source_code_embedding::store_client::{IntermediateNode, Stor use ai::index::full_source_code_embedding::{ self, CodebaseContextConfig, ContentHash, EmbeddingConfig, NodeHash, RepoMetadata, }; -use anyhow::anyhow; +use anyhow::{Context as _, anyhow}; use async_trait::async_trait; use base64::Engine; use bytes::Bytes; @@ -121,6 +121,7 @@ use warp_graphql::queries::task_git_credentials::{ TaskGitCredentialsLegacyVariables, TaskGitCredentialsResult, TaskGitCredentialsVariables, }; use warp_multi_agent_api::ConversationData; +use warp_server_client::base_client::TEAM_UID_HEADER; use super::ServerApi; #[cfg(not(target_family = "wasm"))] @@ -160,6 +161,7 @@ use crate::ai_assistant::{AIGeneratedCommand, GenerateCommandsFromNaturalLanguag use crate::drive::workflows::ai_assist::{GeneratedCommandMetadata, GeneratedCommandMetadataError}; use crate::persistence::model::ConversationUsageMetadata; use crate::server::graphql::{get_request_context, get_user_facing_error_message}; +use crate::server::team_scope::RequestTeamScope; use crate::terminal::model::block::SerializedBlock; #[cfg(not(feature = "agent_mode_evals"))] use crate::{ @@ -1310,6 +1312,7 @@ pub trait AIClient: 'static + Send + Sync { &self, limit: i32, filter: TaskListFilter, + request_team_scope: Option, ) -> anyhow::Result, anyhow::Error>; /// List agent runs and return the raw server JSON response. @@ -1317,6 +1320,7 @@ pub trait AIClient: 'static + Send + Sync { &self, limit: i32, filter: TaskListFilter, + request_team_scope: Option, ) -> anyhow::Result; async fn get_ambient_agent_task( @@ -1624,7 +1628,52 @@ fn into_file_artifact_record( } } +fn with_request_team_scope( + mut request: http_client::RequestBuilder<'_>, + request_team_scope: Option, +) -> http_client::RequestBuilder<'_> { + if let Some(team_uid) = request_team_scope.and_then(RequestTeamScope::team_uid) { + request = request.header(TEAM_UID_HEADER, team_uid.uid()); + } + request +} + impl ServerApi { + async fn get_public_api_with_team_scope( + &self, + path: &str, + request_team_scope: Option, + ) -> anyhow::Result + where + R: serde::de::DeserializeOwned, + { + let auth_token = self + .get_or_refresh_access_token() + .await + .context("Failed to get access token for API request")?; + let url = format!("{}/api/v1/{path}", ChannelState::server_root_url()); + let mut request = self.base_client.http_client().get(&url); + if let Some(token) = auth_token.as_bearer_token() { + request = request.bearer_auth(token); + } + for (name, value) in self.ambient_agent_headers().await? { + request = request.header(name, value); + } + let request = with_request_team_scope(request, request_team_scope); + let response = request + .send() + .await + .with_context(|| format!("Failed to send API request to {url}"))?; + if !response.status().is_success() { + self.observe_iap_challenge(&response); + return Err(Self::error_from_response(response).await); + } + response + .json::() + .await + .with_context(|| format!("Failed to deserialize response from {url}")) + } + pub(crate) async fn send_agent_message_for_task( &self, task_id: &AmbientAgentTaskId, @@ -2410,9 +2459,12 @@ impl AIClient for ServerApi { &self, limit: i32, filter: TaskListFilter, + request_team_scope: Option, ) -> anyhow::Result, anyhow::Error> { let url = build_list_agent_runs_url(limit, &filter); - let response: ListRunsResponse = self.get_public_api(&url).await?; + let response: ListRunsResponse = self + .get_public_api_with_team_scope(&url, request_team_scope) + .await?; Ok(response.runs) } @@ -2420,9 +2472,12 @@ impl AIClient for ServerApi { &self, limit: i32, filter: TaskListFilter, + request_team_scope: Option, ) -> anyhow::Result { let url = build_list_agent_runs_url(limit, &filter); - let response: serde_json::Value = self.get_public_api(&url).await?; + let response: serde_json::Value = self + .get_public_api_with_team_scope(&url, request_team_scope) + .await?; Ok(response) } diff --git a/app/src/server/server_api/ai_tests.rs b/app/src/server/server_api/ai_tests.rs index 554d995f73a..f857ea98203 100644 --- a/app/src/server/server_api/ai_tests.rs +++ b/app/src/server/server_api/ai_tests.rs @@ -2,7 +2,7 @@ use chrono::{TimeZone, Utc}; use futures::executor::block_on; use itertools::Itertools; use mockito::{Matcher, Server}; -use warp_server_client::base_client::CLOUD_AGENT_ID_HEADER; +use warp_server_client::base_client::{CLOUD_AGENT_ID_HEADER, TEAM_UID_HEADER}; use super::super::ServerApi; use super::{ @@ -13,9 +13,13 @@ use super::{ ReadAgentMessageResponse, RunFollowupRequest, RunSortBy, RunSortOrder, SpawnAgentRequest, TaskListFilter, UploadFieldValue, UserQueryMode, build_fork_conversation_url, build_list_agent_runs_url, build_run_followup_url, is_unknown_git_credential_schema_error, + with_request_team_scope, }; use crate::notebooks::NotebookId; +use crate::server::ids::ServerId; use crate::server::server_api::presigned_upload::upload_to_target; +use crate::server::team_scope::RequestTeamScope; +use crate::workspaces::user_workspaces::{TeamContextForOperation, TeamlessScopeForTest}; #[test] fn ambient_agent_headers_for_task_overrides_existing_cloud_agent_header() { @@ -41,6 +45,41 @@ fn ambient_agent_headers_for_task_overrides_existing_cloud_agent_header() { ); } +#[test] +fn list_agent_runs_sends_selected_team_header() { + let team_uid = ServerId::from(123); + let scope = RequestTeamScope::from_scope(&TeamContextForOperation::new_for_test(team_uid)); + let mut server = Server::new(); + let request = server + .mock("GET", "/agent/runs") + .match_header(TEAM_UID_HEADER, team_uid.to_string().as_str()) + .with_status(200) + .create(); + let client = http_client::Client::new_for_test(); + let request_builder = client.get(format!("{}/agent/runs", server.url())); + + block_on(with_request_team_scope(request_builder, Some(scope)).send()).unwrap(); + + request.assert(); +} + +#[test] +fn list_agent_runs_omits_team_header_for_teamless_scope() { + let scope = RequestTeamScope::from_scope(&TeamlessScopeForTest); + let mut server = Server::new(); + let request = server + .mock("GET", "/agent/runs") + .match_header(TEAM_UID_HEADER, Matcher::Missing) + .with_status(200) + .create(); + let client = http_client::Client::new_for_test(); + let request_builder = client.get(format!("{}/agent/runs", server.url())); + + block_on(with_request_team_scope(request_builder, Some(scope)).send()).unwrap(); + + request.assert(); +} + #[test] fn spawn_agent_request_serializes_agent_uid_as_agent_identity_uid() { let request = SpawnAgentRequest { diff --git a/crates/warp_cli/src/task.rs b/crates/warp_cli/src/task.rs index 0d6af3f87f7..8b0c275bd2a 100644 --- a/crates/warp_cli/src/task.rs +++ b/crates/warp_cli/src/task.rs @@ -4,12 +4,13 @@ use clap::{Args, Subcommand, ValueEnum}; use crate::SortOrderArg; use crate::date_time::parse_rfc3339; use crate::json_filter::JsonOutput; +use crate::scope::TeamSelection; /// Task-related subcommands. #[derive(Debug, Clone, Subcommand)] pub enum TaskCommand { /// List ambient agent tasks. - List(ListTasksArgs), + List(Box), /// Get status of a specific ambient agent task. Get(TaskGetArgs), /// Retrieve the conversation for a specific run or conversation. @@ -130,6 +131,8 @@ pub struct MessageDeliveredArgs { #[derive(Debug, Clone, Args)] pub struct ListTasksArgs { + #[command(flatten)] + pub team_selection: TeamSelection, /// Maximum number of tasks to return (default: 10). #[arg(short = 'L', long = "limit", default_value = "10")] pub limit: i32, diff --git a/crates/warp_cli/src/task_tests.rs b/crates/warp_cli/src/task_tests.rs index a8ec05e7cb9..49b2b9c2065 100644 --- a/crates/warp_cli/src/task_tests.rs +++ b/crates/warp_cli/src/task_tests.rs @@ -27,6 +27,7 @@ fn parse_err(argv: &[&str]) -> clap::Error { #[test] fn defaults_match_pre_change_behavior() { let TestApp { args } = parse(&[]); + assert!(args.team_selection.team.is_none()); assert_eq!(args.limit, 10); assert!(args.state.is_empty()); assert!(args.source.is_none()); @@ -48,6 +49,13 @@ fn defaults_match_pre_change_behavior() { assert!(args.cursor.is_none()); } +#[test] +fn team_selection_parses_for_run_listing() { + let TestApp { args } = parse(&["--team=team-uid"]); + + assert_eq!(args.team_selection.requested_team_uid(), Some("team-uid")); +} + #[test] fn state_flag_is_repeatable() { let TestApp { args } = parse(&["--state", "failed", "--state", "error"]);