diff --git a/app/src/ai/agent_sdk/ambient.rs b/app/src/ai/agent_sdk/ambient.rs index 4ac5ed52227..8645c1d49ae 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; @@ -393,6 +394,7 @@ impl AmbientAgentRunner { return; } }; + let request_team_scope = RequestTeamScope::from_scope(&team_scope); let mut environment_args = args.environment; if environment_args.environment.is_none() && !environment_args.no_environment @@ -559,7 +561,12 @@ impl AmbientAgentRunner { let oz_root_url = ChannelState::oz_root_url(); let ai_client_clone = ai_client.clone(); let spawn_future = async move { - let mut stream = Box::pin(spawn_task(request, ai_client_clone, Some(TASK_STATUS_POLLING_DURATION))); + let mut stream = Box::pin(spawn_task( + request, + request_team_scope, + ai_client_clone, + Some(TASK_STATUS_POLLING_DURATION), + )); let mut session_join_info = None; let mut spawned_task_id = None; diff --git a/app/src/ai/ambient_agents/spawn.rs b/app/src/ai/ambient_agents/spawn.rs index 21ba327154f..5279738adb4 100644 --- a/app/src/ai/ambient_agents/spawn.rs +++ b/app/src/ai/ambient_agents/spawn.rs @@ -14,6 +14,7 @@ use crate::server::retry_strategies::with_bounded_retry; use crate::server::server_api::ai::{ AIClient, RunFollowupRequest, SpawnAgentRequest, TaskStatusMessage, }; +use crate::server::team_scope::RequestTeamScope; use crate::terminal::shared_session; /// How long to poll for the agent to be ready. @@ -101,6 +102,7 @@ enum RunPollMode { /// If `timeout` is `None`, there is no timeout. pub fn spawn_task( request: SpawnAgentRequest, + team_scope: RequestTeamScope, ai_client: Arc, timeout: Option, ) -> impl Stream> { @@ -108,7 +110,7 @@ pub fn spawn_task( // See https://github.com/tokio-rs/async-stream/issues/63. async_stream::stream! { // First, spawn the ambient agent task. - let (task_id, run_id, at_capacity) = match ai_client.spawn_agent(request).await { + let (task_id, run_id, at_capacity) = match ai_client.spawn_agent(request, team_scope).await { Ok(response) => (response.task_id, response.run_id, response.at_capacity), Err(err) => { yield Err(err); diff --git a/app/src/ai/ambient_agents/spawn_tests.rs b/app/src/ai/ambient_agents/spawn_tests.rs index 20883882678..13e0057575e 100644 --- a/app/src/ai/ambient_agents/spawn_tests.rs +++ b/app/src/ai/ambient_agents/spawn_tests.rs @@ -10,8 +10,15 @@ use super::{ }; use crate::ai::agent::UserQueryMode; use crate::ai::ambient_agents::{AmbientAgentTask, AmbientAgentTaskState}; +use crate::server::ids::ServerId; use crate::server::server_api::ai::{MockAIClient, SpawnAgentResponse, TaskStatusMessage}; +use crate::server::team_scope::RequestTeamScope; use crate::terminal::shared_session; +use crate::workspaces::user_workspaces::{TeamContextForOperation, TeamlessScopeForTest}; + +fn request_team_scope() -> RequestTeamScope { + RequestTeamScope::from_scope(&TeamlessScopeForTest) +} fn task_with( state: AmbientAgentTaskState, @@ -717,8 +724,11 @@ async fn poll_retries_transient_429_errors() { let mut mock = MockAIClient::new(); let call_count = Arc::new(AtomicUsize::new(0)); + let expected_scope = + RequestTeamScope::from_scope(&TeamContextForOperation::new_for_test(ServerId::from(43))); - mock.expect_spawn_agent().returning(|_| { + mock.expect_spawn_agent().returning(move |_, team_scope| { + assert_eq!(team_scope, expected_scope); Ok(SpawnAgentResponse { task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(), run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), @@ -759,7 +769,7 @@ async fn poll_retries_transient_429_errors() { orchestration_handoff: None, }; - let mut stream = Box::pin(spawn_task(request, ai_client, None)); + let mut stream = Box::pin(spawn_task(request, expected_scope, ai_client, None)); // First event: TaskSpawned let event = stream @@ -795,7 +805,7 @@ async fn poll_fails_on_permanent_http_error() { let mut mock = MockAIClient::new(); - mock.expect_spawn_agent().returning(|_| { + mock.expect_spawn_agent().returning(|_, _| { Ok(SpawnAgentResponse { task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(), run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), @@ -828,7 +838,7 @@ async fn poll_fails_on_permanent_http_error() { orchestration_handoff: None, }; - let mut stream = Box::pin(spawn_task(request, ai_client, None)); + let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None)); // First event: TaskSpawned let event = stream @@ -861,7 +871,7 @@ async fn poll_gives_up_after_max_transient_retries() { let mut mock = MockAIClient::new(); let call_count = Arc::new(AtomicUsize::new(0)); - mock.expect_spawn_agent().returning(|_| { + mock.expect_spawn_agent().returning(|_, _| { Ok(SpawnAgentResponse { task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(), run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), @@ -898,7 +908,7 @@ async fn poll_gives_up_after_max_transient_retries() { orchestration_handoff: None, }; - let mut stream = Box::pin(spawn_task(request, ai_client, None)); + let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None)); // First event: TaskSpawned let event = stream @@ -930,7 +940,7 @@ async fn poll_stops_on_terminal_failure_like_state() { let mut mock = MockAIClient::new(); - mock.expect_spawn_agent().returning(|_| { + mock.expect_spawn_agent().returning(|_, _| { Ok(SpawnAgentResponse { task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(), run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), @@ -962,7 +972,7 @@ async fn poll_stops_on_terminal_failure_like_state() { orchestration_handoff: None, }; - let mut stream = Box::pin(spawn_task(request, ai_client, None)); + let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None)); let event = stream .next() @@ -1063,7 +1073,7 @@ async fn poll_for_session_join_info_waits_until_link_is_available() { let call_count = Arc::new(AtomicUsize::new(0)); - mock.expect_spawn_agent().returning(|_| { + mock.expect_spawn_agent().returning(|_, _| { Ok(SpawnAgentResponse { task_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(), run_id: "550e8400-e29b-41d4-a716-446655440000".to_string(), @@ -1109,7 +1119,7 @@ async fn poll_for_session_join_info_waits_until_link_is_available() { orchestration_handoff: None, }; - let mut stream = Box::pin(spawn_task(request, ai_client, None)); + let mut stream = Box::pin(spawn_task(request, request_team_scope(), ai_client, None)); // First event should be TaskSpawned let event = stream diff --git a/app/src/ai/blocklist/handoff/pipeline.rs b/app/src/ai/blocklist/handoff/pipeline.rs index bc036820328..6ad2b2f89f7 100644 --- a/app/src/ai/blocklist/handoff/pipeline.rs +++ b/app/src/ai/blocklist/handoff/pipeline.rs @@ -57,6 +57,7 @@ use crate::server::ids::{ServerId, SyncId}; use crate::server::server_api::ai::{ AIClient, AgentConfigSnapshot, AttachmentInput, InitialSnapshotToken, SpawnAgentRequest, }; +use crate::server::team_scope::RequestTeamScope; use crate::settings::AISettings; use crate::workspaces::user_workspaces::ResolvedTeamScope; @@ -685,6 +686,7 @@ struct SnapshotSettledHandoff { /// exclusive. pub fn execute_handoff( mut pending: PendingHandoff, + team_scope: RequestTeamScope, ai_client: Arc, caller_cancellation: Option>, materialize_handoff_target: Option, @@ -715,6 +717,7 @@ pub fn execute_handoff( Box::pin(execute_validated_handoff( pending, + team_scope, ai_client, caller_cancellation, materialize_handoff_target, @@ -723,6 +726,7 @@ pub fn execute_handoff( async fn execute_validated_handoff( pending: PendingHandoff, + team_scope: RequestTeamScope, ai_client: Arc, caller_cancellation: Option>, materialize_handoff_target: Option, @@ -799,7 +803,7 @@ async fn execute_validated_handoff( settled.forked_conversation_id, settled.initial_snapshot_token, ); - let response = ai_client.spawn_agent(request.clone()).await; + let response = ai_client.spawn_agent(request.clone(), team_scope).await; if cancellation .as_mut() .is_some_and(handoff_cancellation_requested) diff --git a/app/src/ai/blocklist/handoff/pipeline_tests.rs b/app/src/ai/blocklist/handoff/pipeline_tests.rs index 8573a47817b..56d2f1422e9 100644 --- a/app/src/ai/blocklist/handoff/pipeline_tests.rs +++ b/app/src/ai/blocklist/handoff/pipeline_tests.rs @@ -23,8 +23,14 @@ use crate::features::FeatureFlag; use crate::server::ids::{ServerId, SyncId}; use crate::server::server_api::ServerApiProvider; use crate::server::server_api::ai::{ForkConversationResponse, MockAIClient, SpawnAgentResponse}; +use crate::server::team_scope::RequestTeamScope; use crate::test_util::add_window_with_terminal; use crate::test_util::terminal::initialize_app_for_terminal_view; +use crate::workspaces::user_workspaces::TeamlessScopeForTest; + +fn request_team_scope() -> RequestTeamScope { + RequestTeamScope::from_scope(&TeamlessScopeForTest) +} fn task_id() -> AmbientAgentTaskId { "550e8400-e29b-41d4-a716-446655440000" @@ -185,7 +191,8 @@ fn execute_revalidates_current_model_before_returning_future() { pending.selected_model_id = "custom-router:local:byok".to_owned(); pending.model_is_cloud_runnable = true; - let future = app.update(|ctx| execute_handoff(pending, client, None, None, ctx)); + let future = app + .update(|ctx| execute_handoff(pending, request_team_scope(), client, None, None, ctx)); let HandoffCommitOutcome::Rejected { error, .. } = future.await else { panic!("current invalid model must reject before external work"); }; @@ -208,7 +215,8 @@ fn execute_revalidates_current_environment_catalog_before_returning_future() { pending.valid_environment_ids.insert(environment_id); pending.config.environment_id = Some(environment_id.to_string()); - let future = app.update(|ctx| execute_handoff(pending, client, None, None, ctx)); + let future = app + .update(|ctx| execute_handoff(pending, request_team_scope(), client, None, None, ctx)); let HandoffCommitOutcome::Rejected { error, .. } = future.await else { panic!("deleted environment must reject before external work"); }; @@ -227,7 +235,8 @@ fn execute_revalidates_current_handoff_enablement_before_returning_future() { let client: Arc = Arc::new(mock); let pending = pending(client.clone(), None, false, "continue"); - let future = app.update(|ctx| execute_handoff(pending, client, None, None, ctx)); + let future = app + .update(|ctx| execute_handoff(pending, request_team_scope(), client, None, None, ctx)); let HandoffCommitOutcome::Rejected { error, .. } = future.await else { panic!("disabled handoff must reject before external work"); }; @@ -764,6 +773,11 @@ async fn fork_materialization_precedes_exactly_one_spawn() { let materialized = Arc::new(AtomicBool::new(false)); let spawn_count = Arc::new(AtomicUsize::new(0)); let observed_request = Arc::new(Mutex::new(None)); + let expected_scope = RequestTeamScope::from_scope( + &crate::workspaces::user_workspaces::TeamContextForOperation::new_for_test(ServerId::from( + 42, + )), + ); let mut mock = MockAIClient::new(); mock.expect_fork_conversation() .times(1) @@ -779,8 +793,9 @@ async fn fork_materialization_precedes_exactly_one_spawn() { let materialized = materialized.clone(); let spawn_count = spawn_count.clone(); let observed_request = observed_request.clone(); - move |request| { + move |request, team_scope| { assert!(materialized.load(Ordering::SeqCst)); + assert_eq!(team_scope, expected_scope); spawn_count.fetch_add(1, Ordering::SeqCst); *observed_request.lock().expect("request lock") = Some(request); Ok(SpawnAgentResponse { @@ -814,6 +829,7 @@ async fn fork_materialization_precedes_exactly_one_spawn() { true, "", ), + expected_scope, client, None, Some(materialize), @@ -856,7 +872,7 @@ async fn fresh_launch_skips_fork_and_materializes_before_spawn() { mock.expect_fork_conversation().times(0); mock.expect_spawn_agent().times(1).returning({ let materialized = materialized.clone(); - move |request| { + move |request, _| { assert!(materialized.load(Ordering::SeqCst)); assert!(request.conversation_id.is_none()); Ok(SpawnAgentResponse { @@ -882,6 +898,7 @@ async fn fresh_launch_skips_fork_and_materializes_before_spawn() { let outcome = execute_validated_handoff( pending(client.clone(), None, false, "new task"), + request_team_scope(), client, None, Some(materialize), @@ -912,6 +929,7 @@ async fn cancellation_after_materialization_stops_before_spawn() { let outcome = execute_validated_handoff( pending(client.clone(), None, false, "new task"), + request_team_scope(), client, None, Some(materialize), @@ -928,7 +946,7 @@ async fn cancellation_during_spawn_cancels_the_created_task() { mock.expect_fork_conversation().times(0); mock.expect_spawn_agent().times(1).returning({ let cancel = cancel.clone(); - move |_| { + move |_, _| { cancel .lock() .expect("cancel sender lock") @@ -958,6 +976,7 @@ async fn cancellation_during_spawn_cancels_the_created_task() { let outcome = execute_validated_handoff( pending(client.clone(), None, false, "new task"), + request_team_scope(), client, None, Some(materialize), @@ -984,7 +1003,7 @@ async fn snapshot_failure_degrades_to_spawn_without_token() { mock.expect_upload_local_handoff_snapshot() .times(1) .returning(|_| Err(anyhow::anyhow!("snapshot unavailable"))); - mock.expect_spawn_agent().times(1).returning(|request| { + mock.expect_spawn_agent().times(1).returning(|request, _| { assert!(request.initial_snapshot_token.is_none()); Ok(SpawnAgentResponse { task_id: task_id(), @@ -996,7 +1015,8 @@ async fn snapshot_failure_degrades_to_spawn_without_token() { let mut pending = pending(client.clone(), None, false, "continue"); pending.source_paths = vec![path]; - let outcome = execute_validated_handoff(pending, client, None, None).await; + let outcome = + execute_validated_handoff(pending, request_team_scope(), client, None, None).await; let HandoffCommitOutcome::Created(created) = outcome else { panic!("snapshot failure should not fail the handoff"); }; @@ -1016,6 +1036,7 @@ async fn caller_cancellation_stops_before_spawn() { let outcome = execute_validated_handoff( pending(client.clone(), None, false, "new task"), + request_team_scope(), client, Some(cancellation), None, diff --git a/app/src/pane_group/pane/terminal_pane.rs b/app/src/pane_group/pane/terminal_pane.rs index d54e49d24c3..0919785adef 100644 --- a/app/src/pane_group/pane/terminal_pane.rs +++ b/app/src/pane_group/pane/terminal_pane.rs @@ -54,6 +54,7 @@ use crate::pane_group::{self, Direction, PaneGroup}; use crate::persistence::{BlockCompleted, ModelEvent}; #[cfg(not(target_family = "wasm"))] use crate::server::server_api::ServerApiProvider; +use crate::server::team_scope::RequestTeamScope; use crate::session_management::SessionNavigationData; use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel; use crate::terminal::general_settings::GeneralSettings; @@ -68,7 +69,8 @@ use crate::view_components::ToastFlavor; use crate::workspace::sync_inputs::SyncedInputState; use crate::workspace::{PaneViewLocator, WorkspaceRegistry}; #[cfg(not(target_family = "wasm"))] -use crate::workspaces::user_workspaces::{ResolvedTeamScope, UserWorkspaces}; +use crate::workspaces::user_workspaces::ResolvedTeamScope; +use crate::workspaces::user_workspaces::UserWorkspaces; #[cfg(not(target_family = "wasm"))] use crate::{ pane_group::child_agent::{ @@ -1986,6 +1988,8 @@ fn launch_remote_child( return None; } }; + let team_scope = + RequestTeamScope::from_scope(&UserWorkspaces::as_ref(ctx).team_context_for_operation(ctx)); new_terminal_view.update(ctx, |terminal_view, ctx| { terminal_view.enter_agent_view( @@ -1997,7 +2001,7 @@ fn launch_remote_child( if let Some(ambient_agent_view_model) = terminal_view.ambient_agent_view_model() { ambient_agent_view_model.update(ctx, |model, ctx| { model.set_conversation_id(Some(conversation_id)); - model.spawn_agent_with_request(prepared.spawn_request, ctx); + model.spawn_agent_with_request(prepared.spawn_request, team_scope, ctx); }); } else { report_error!("Remote StartAgent child pane missing ambient agent view model"); diff --git a/app/src/server/server_api.rs b/app/src/server/server_api.rs index 5d5821597d2..29b4e3bf3cf 100644 --- a/app/src/server/server_api.rs +++ b/app/src/server/server_api.rs @@ -682,12 +682,35 @@ impl ServerApi { Ok(self.wrap_eventsource_with_iap_detection(request.eventsource())) } + fn with_request_team_scope( + request: http_client::RequestBuilder<'_>, + team_scope: Option, + ) -> http_client::RequestBuilder<'_> { + match team_scope.and_then(RequestTeamScope::team_uid) { + Some(team_uid) => request.header(TEAM_UID_HEADER, team_uid.uid()), + None => request, + } + } + /// Sends a POST request to a public API endpoint and returns the raw response on success. async fn post_public_api_response( &self, path: &str, body: &B, ) -> Result + where + B: Serialize, + { + self.post_public_api_response_with_scope(path, body, None) + .await + } + + async fn post_public_api_response_with_scope( + &self, + path: &str, + body: &B, + team_scope: Option, + ) -> Result where B: Serialize, { @@ -702,6 +725,7 @@ impl ServerApi { if let Some(token) = auth_token.as_bearer_token() { request = request.bearer_auth(token); } + request = Self::with_request_team_scope(request, team_scope); for (name, value) in self.ambient_agent_headers().await? { request = request.header(name, value); @@ -787,6 +811,26 @@ impl ServerApi { .with_context(|| format!("Failed to deserialize response from {url}")) } + async fn post_public_api_with_scope( + &self, + path: &str, + body: &B, + team_scope: RequestTeamScope, + ) -> Result + where + B: Serialize, + R: serde::de::DeserializeOwned, + { + let response = self + .post_public_api_response_with_scope(path, body, Some(team_scope)) + .await?; + let url = response.url().clone(); + response + .json::() + .await + .with_context(|| format!("Failed to deserialize response from {url}")) + } + /// Sends a PUT request to a public API endpoint and returns the raw response on success. async fn put_public_api_response( &self, diff --git a/app/src/server/server_api/ai.rs b/app/src/server/server_api/ai.rs index 31eace9b15d..32cfb1eb16c 100644 --- a/app/src/server/server_api/ai.rs +++ b/app/src/server/server_api/ai.rs @@ -160,6 +160,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::{ @@ -1283,6 +1284,7 @@ pub trait AIClient: 'static + Send + Sync { async fn spawn_agent( &self, request: SpawnAgentRequest, + team_scope: RequestTeamScope, ) -> anyhow::Result; /// Allocate an initial snapshot token and presigned upload URLs for staging local-to-cloud @@ -2360,8 +2362,11 @@ impl AIClient for ServerApi { async fn spawn_agent( &self, request: SpawnAgentRequest, + team_scope: RequestTeamScope, ) -> anyhow::Result { - let response: SpawnAgentResponse = self.post_public_api("agent/run", &request).await?; + let response: SpawnAgentResponse = self + .post_public_api_with_scope("agent/run", &request, team_scope) + .await?; Ok(response) } diff --git a/app/src/server/server_api_tests.rs b/app/src/server/server_api_tests.rs index 44afb5de291..daf1624d4f1 100644 --- a/app/src/server/server_api_tests.rs +++ b/app/src/server/server_api_tests.rs @@ -1,8 +1,12 @@ +use std::sync::{Arc, Mutex}; + use futures::executor::block_on; use mockito::Server; use super::*; +use crate::server::ids::ServerId; use crate::server::retry_strategies::is_transient_http_error; +use crate::workspaces::user_workspaces::{TeamContextForOperation, TeamlessScopeForTest}; /// Sends a GET request to a mock endpoint returning `status`/`headers`/`body`, then feeds the /// resulting response through [`ServerApi::error_from_response`]. @@ -28,6 +32,53 @@ fn error_from_mock_response(status: usize, headers: &[(&str, &str)], body: &str) }) } +#[test] +fn request_team_scope_controls_the_team_header() { + let observed_headers = Arc::new(Mutex::new(Vec::new())); + let observed_headers_for_hook = observed_headers.clone(); + let mut client = http_client::Client::new_for_test(); + client.set_before_request_fn(Box::new(move |request, _| { + observed_headers_for_hook + .lock() + .expect("header observations lock") + .push( + request + .headers() + .get(TEAM_UID_HEADER) + .map(|value| value.to_str().expect("team header is valid").to_owned()), + ); + })); + let mut server = Server::new(); + let request = server + .mock("POST", "/scope") + .with_status(200) + .expect(2) + .create(); + let url = format!("{}/scope", server.url()); + let selected_team_uid = ServerId::from(42); + let expected_header = selected_team_uid.uid().to_owned(); + let selected_scope = + RequestTeamScope::from_scope(&TeamContextForOperation::new_for_test(selected_team_uid)); + let teamless_scope = RequestTeamScope::from_scope(&TeamlessScopeForTest); + + block_on(async { + ServerApi::with_request_team_scope(client.post(&url), Some(selected_scope)) + .send() + .await + .expect("selected-team request"); + ServerApi::with_request_team_scope(client.post(&url), Some(teamless_scope)) + .send() + .await + .expect("teamless request"); + }); + request.assert(); + + assert_eq!( + *observed_headers.lock().expect("header observations lock"), + vec![Some(expected_header), None] + ); +} + /// The status carried by the [`HttpStatusError`] in `err`'s chain, if any. fn status_in_chain(err: &anyhow::Error) -> Option { err.chain() diff --git a/app/src/terminal/view/ambient_agent/model.rs b/app/src/terminal/view/ambient_agent/model.rs index 8e828dffb1e..9445f41e9e7 100644 --- a/app/src/terminal/view/ambient_agent/model.rs +++ b/app/src/terminal/view/ambient_agent/model.rs @@ -45,6 +45,7 @@ use crate::server::server_api::ai::{ AgentConfigSnapshot, AmbientAgentTaskState, AttachmentInput, RunFollowupRequest, SpawnAgentRequest, }; +use crate::server::team_scope::RequestTeamScope; use crate::terminal::view::ambient_agent::{SetupCommandGroupId, SetupCommandState}; use crate::terminal::{CLIAgent, TerminalView}; use crate::workspaces::user_workspaces::{TeamScope, UserWorkspaces}; @@ -132,6 +133,7 @@ pub struct AmbientAgentViewModel { /// The request with which the cloud agent was spawned, if it was spawned. request: Option, + request_team_scope: Option, /// The terminal view this model is part of. terminal_view_id: EntityId, @@ -246,6 +248,7 @@ impl AmbientAgentViewModel { Self { status: Status::Composing, request: None, + request_team_scope: None, terminal_view_id, terminal_view, environment_id: None, @@ -1132,6 +1135,7 @@ impl AmbientAgentViewModel { ctx: &mut ModelContext, ) { let config = Some(self.build_default_spawn_config(scope, ctx)); + let request_team_scope = RequestTeamScope::from_scope(scope); let (prompt, mode) = extract_user_query_mode(prompt); let request = SpawnAgentRequest { @@ -1153,13 +1157,14 @@ impl AmbientAgentViewModel { orchestration_handoff: None, }; - self.spawn_internal(request, ctx); + self.spawn_internal(request, request_team_scope, ctx); } /// Spawn an ambient agent with a fully-constructed request. pub fn spawn_agent_with_request( &mut self, request: SpawnAgentRequest, + team_scope: RequestTeamScope, ctx: &mut ModelContext, ) { // Apply pane settings from the request. @@ -1189,16 +1194,22 @@ impl AmbientAgentViewModel { } } - self.spawn_internal(request, ctx); + self.spawn_internal(request, team_scope, ctx); } /// Stores `request` and starts the combined spawn-and-monitor stream. - fn start_spawn_stream(&mut self, mut request: SpawnAgentRequest, ctx: &mut ModelContext) { + fn start_spawn_stream( + &mut self, + mut request: SpawnAgentRequest, + team_scope: RequestTeamScope, + ctx: &mut ModelContext, + ) { request.interactive = Some(true); self.request = Some(request.clone()); + self.request_team_scope = Some(team_scope); self.source = None; let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client(); - let stream = spawn_task(request, ai_client, None); + let stream = spawn_task(request, team_scope, ai_client, None); ctx.spawn_stream_local( stream, |me, event_result, ctx| me.handle_ambient_agent_event_result(event_result, ctx), @@ -1207,8 +1218,13 @@ impl AmbientAgentViewModel { } /// Spawn an ambient agent given `request`. - fn spawn_internal(&mut self, request: SpawnAgentRequest, ctx: &mut ModelContext) { - self.start_spawn_stream(request, ctx); + fn spawn_internal( + &mut self, + request: SpawnAgentRequest, + team_scope: RequestTeamScope, + ctx: &mut ModelContext, + ) { + self.start_spawn_stream(request, team_scope, ctx); self.status = Status::WaitingForSession { progress: AgentProgress::new(), kind: SessionStartupKind::InitialRun, @@ -1516,8 +1532,11 @@ impl AmbientAgentViewModel { let Some(request) = self.request.clone() else { return; }; + let Some(team_scope) = self.request_team_scope else { + return; + }; - self.spawn_internal(request, ctx); + self.spawn_internal(request, team_scope, ctx); } /// Handles cancellation by transitioning to the Cancelled state. diff --git a/app/src/terminal/view/ambient_agent/model_tests.rs b/app/src/terminal/view/ambient_agent/model_tests.rs index a76e0b5aa4c..26144d980b1 100644 --- a/app/src/terminal/view/ambient_agent/model_tests.rs +++ b/app/src/terminal/view/ambient_agent/model_tests.rs @@ -5,7 +5,7 @@ use super::*; use crate::ai::llms::{AvailableLLMs, LLMId, LLMInfo, LLMPreferences, ModelsByFeature}; use crate::server::server_api::ClientError; use crate::test_util::terminal::{add_window_with_terminal, initialize_app_for_terminal_view}; -use crate::workspaces::user_workspaces::TeamlessScopeForTest; +use crate::workspaces::user_workspaces::{TeamContextForOperation, TeamlessScopeForTest}; fn attachment() -> AttachmentInput { AttachmentInput { @@ -413,6 +413,9 @@ fn github_auth_completed_retries_stored_initial_run_request() { App::test((), |mut app| async move { initialize_app_for_terminal_view(&mut app); let model = add_model(&mut app); + let request_team_scope = RequestTeamScope::from_scope( + &TeamContextForOperation::new_for_test(ServerId::from(44)), + ); model.update(&mut app, |model, ctx| { model.status = Status::NeedsGithubAuth { @@ -421,6 +424,7 @@ fn github_auth_completed_retries_stored_initial_run_request() { auth_url: "https://example.com/oauth/connect/github".to_string(), }; model.request = Some(retry_request("retry this")); + model.request_team_scope = Some(request_team_scope); model.handle_github_auth_completed(ctx); @@ -432,6 +436,7 @@ fn github_auth_completed_retries_stored_initial_run_request() { } )); let request = model.request().expect("retry should spawn a request"); + assert_eq!(model.request_team_scope, Some(request_team_scope)); assert_eq!(request.prompt.as_deref(), Some("retry this")); assert_eq!(request.attachments.len(), 1); assert_eq!(request.interactive, Some(true)); diff --git a/app/src/terminal/view/queued_prompts_tests.rs b/app/src/terminal/view/queued_prompts_tests.rs index bd6080803e7..c9b03b3bdd0 100644 --- a/app/src/terminal/view/queued_prompts_tests.rs +++ b/app/src/terminal/view/queued_prompts_tests.rs @@ -29,11 +29,17 @@ use crate::ai::blocklist::{ use crate::features::FeatureFlag; use crate::search::slash_command_menu::static_commands::commands; use crate::server::server_api::ai::SpawnAgentRequest; +use crate::server::team_scope::RequestTeamScope; use crate::terminal::input::{Event as InputEvent, Input}; use crate::terminal::shared_session::SharedSessionStatus; use crate::terminal::view::ambient_agent::AmbientAgentViewModelEvent; use crate::test_util::settings::initialize_settings_for_tests; use crate::test_util::terminal::{add_window_with_terminal, initialize_app_for_terminal_view}; +use crate::workspaces::user_workspaces::TeamlessScopeForTest; + +fn request_team_scope() -> RequestTeamScope { + RequestTeamScope::from_scope(&TeamlessScopeForTest) +} fn user_query(text: &str) -> QueuedQuery { QueuedQuery::new(text.to_owned(), QueuedQueryOrigin::QueueSlashCommand) @@ -219,7 +225,11 @@ fn dispatched_cloud_prompt_uses_locked_queue_row_when_v2_is_enabled() { view.ambient_agent_view_model() .expect("cloud terminal should have an ambient model") .update(ctx, |model, ctx| { - model.spawn_agent_with_request(cloud_spawn_request("write tests"), ctx); + model.spawn_agent_with_request( + cloud_spawn_request("write tests"), + request_team_scope(), + ctx, + ); }); view.handle_ambient_agent_event(&AmbientAgentViewModelEvent::DispatchedAgent, ctx); @@ -387,7 +397,11 @@ fn cloud_setup_enter_queues_followup_input_when_v2_is_enabled() { view.ambient_agent_view_model() .expect("cloud terminal should have an ambient model") .update(ctx, |model, ctx| { - model.spawn_agent_with_request(cloud_spawn_request("initial"), ctx); + model.spawn_agent_with_request( + cloud_spawn_request("initial"), + request_team_scope(), + ctx, + ); }); view.input.update(ctx, |input, ctx| { @@ -423,7 +437,11 @@ fn cloud_setup_enter_does_not_queue_followup_for_third_party_harness() { view.ambient_agent_view_model() .expect("cloud terminal should have an ambient model") .update(ctx, |model, ctx| { - model.spawn_agent_with_request(cloud_spawn_request("initial"), ctx); + model.spawn_agent_with_request( + cloud_spawn_request("initial"), + request_team_scope(), + ctx, + ); model.set_harness(Harness::Claude, ctx); }); @@ -500,7 +518,11 @@ fn cloud_setup_enter_remains_blocked_when_v2_is_disabled() { view.ambient_agent_view_model() .expect("cloud terminal should have an ambient model") .update(ctx, |model, ctx| { - model.spawn_agent_with_request(cloud_spawn_request("initial"), ctx); + model.spawn_agent_with_request( + cloud_spawn_request("initial"), + request_team_scope(), + ctx, + ); }); view.input.update(ctx, |input, ctx| { @@ -622,7 +644,11 @@ fn promptless_setup_complete_auto_sends_queued_prompt_to_viewer() { view.ambient_agent_view_model() .expect("cloud terminal should have an ambient model") .update(ctx, |model, ctx| { - model.spawn_agent_with_request(promptless_cloud_spawn_request(), ctx); + model.spawn_agent_with_request( + promptless_cloud_spawn_request(), + request_team_scope(), + ctx, + ); }); QueuedQueryModel::handle(ctx).update(ctx, |model, ctx| { model.append( @@ -683,7 +709,11 @@ fn promptless_setup_complete_with_initial_prompt_does_not_drain_queue() { view.ambient_agent_view_model() .expect("cloud terminal should have an ambient model") .update(ctx, |model, ctx| { - model.spawn_agent_with_request(cloud_spawn_request("initial prompt"), ctx); + model.spawn_agent_with_request( + cloud_spawn_request("initial prompt"), + request_team_scope(), + ctx, + ); }); QueuedQueryModel::handle(ctx).update(ctx, |model, ctx| { model.append( diff --git a/app/src/terminal/view_tests.rs b/app/src/terminal/view_tests.rs index 43bbbdc901a..54595b72120 100644 --- a/app/src/terminal/view_tests.rs +++ b/app/src/terminal/view_tests.rs @@ -55,6 +55,7 @@ use crate::pane_group::pane::PaneStack; use crate::pane_group::{BackingView, TerminalPaneId}; use crate::server::ids::{ClientId, SyncId}; use crate::server::server_api::ai::SpawnAgentRequest; +use crate::server::team_scope::RequestTeamScope; use crate::settings::import::model::ImportedConfigModel; use crate::settings::{AISettings, AppEditorSettings, RightClickBehavior, WarpPromptSeparator}; use crate::terminal::alt_screen::should_intercept_mouse; @@ -93,6 +94,11 @@ use crate::test_util::terminal::{ use crate::test_util::{add_window_with_terminal, assert_eventually}; use crate::view_components::find::FindWithinBlockState; use crate::workspace::ToastStack; +use crate::workspaces::user_workspaces::TeamlessScopeForTest; + +fn request_team_scope() -> RequestTeamScope { + RequestTeamScope::from_scope(&TeamlessScopeForTest) +} fn add_window_with_cloud_mode_terminal(app: &mut App) -> ViewHandle { let tips_model = app.add_model(|_| Default::default()); @@ -3449,6 +3455,7 @@ fn cloud_mode_dispatched_agent_inserts_queued_user_query() { snapshot_disabled: None, orchestration_handoff: None, }, + request_team_scope(), ctx, ); }); diff --git a/app/src/tui_export.rs b/app/src/tui_export.rs index 57b455ca1f3..35cbe6b6b39 100644 --- a/app/src/tui_export.rs +++ b/app/src/tui_export.rs @@ -191,7 +191,6 @@ pub use crate::server::server_api::TranscribeError; pub use crate::server::server_api::ai::{ AIClient, AgentConfigSnapshot, AttachmentInput, SpawnAgentRequest, SpawnAgentResponse, }; -#[cfg(feature = "voice_input")] pub use crate::server::team_scope::RequestTeamScope; pub use crate::server::telemetry::{SlashMenuSource, TelemetryEvent}; pub use crate::settings::{AISettingsChangedEvent, InputSettings}; diff --git a/app/src/workspace/view.rs b/app/src/workspace/view.rs index 2876acf6f1a..b5f68ceff58 100644 --- a/app/src/workspace/view.rs +++ b/app/src/workspace/view.rs @@ -337,6 +337,7 @@ use crate::server::ids::{ObjectUid, ServerId, SyncId}; use crate::server::network_log_pane_manager::NetworkLogPaneManager; use crate::server::server_api::ai::AIClient; use crate::server::server_api::{ServerApi, ServerApiProvider, ServerTime}; +use crate::server::team_scope::RequestTeamScope; use crate::server::telemetry::{ AddTabWithShellSource, AnonymousUserSignupEntrypoint, CloseTarget, EnvVarTelemetryMetadata, FileTreeSource, KnowledgePaneEntrypoint, LaunchConfigUiLocation, @@ -15753,6 +15754,9 @@ impl Workspace { return; } }; + let team_scope = RequestTeamScope::from_scope( + &UserWorkspaces::as_ref(ctx).team_context_for_operation(ctx), + ); let presentation = pending.presentation_snapshot(); let model_slot: Arc>>> = @@ -15778,7 +15782,8 @@ impl Workspace { }) }); let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client(); - let execution = execute_handoff(pending, ai_client, None, Some(materialize), ctx); + let execution = + execute_handoff(pending, team_scope, ai_client, None, Some(materialize), ctx); ctx.spawn(execution, move |workspace, outcome, ctx| match outcome { HandoffCommitOutcome::Rejected { mut pending, error } => { let restoration = pending.take_restoration(); diff --git a/crates/warp_tui/src/handoff/model.rs b/crates/warp_tui/src/handoff/model.rs index ae5189de3aa..3a27034d58d 100644 --- a/crates/warp_tui/src/handoff/model.rs +++ b/crates/warp_tui/src/handoff/model.rs @@ -19,9 +19,9 @@ use warp::tui_export::{ CloudEnvironmentCatalog, HandoffCommitOutcome, HandoffEntryPoint, HandoffLaunchAttachments, HandoffPrepareError, HandoffPrepareInput, HandoffRestoration, HandoffSurface, LLMId, LLMPreferences, LLMPreferencesEvent, OptionRow, OptionSnapshot, OptionSourceStatus, - PendingCloudLaunch, PendingHandoff, ServerApiProvider, SnapshotUploadTarget, TerminalModel, - UserWorkspaces, UserWorkspacesEvent, execute_handoff, handoff_dispatch_error, - oz_model_snapshot, prepare_handoff, suggest_handoff_environment, + PendingCloudLaunch, PendingHandoff, RequestTeamScope, ServerApiProvider, SnapshotUploadTarget, + TeamContextResolver, TerminalModel, UserWorkspaces, UserWorkspacesEvent, execute_handoff, + handoff_dispatch_error, oz_model_snapshot, prepare_handoff, suggest_handoff_environment, }; use warpui::{AppContext, Entity, EntityId, ModelContext, ModelHandle, SingletonEntity as _}; @@ -102,6 +102,7 @@ pub(crate) struct TuiHandoffModel { source_conversation_id: Option, phase: TuiHandoffPhase, environments: ModelHandle, + team_context_resolver: TeamContextResolver, forked_existing_conversation: bool, next_operation_id: u64, execution_cancellation: Option>, @@ -110,6 +111,7 @@ pub(crate) struct TuiHandoffModel { impl TuiHandoffModel { /// Prepares a handoff and registers its retained model. + #[allow(clippy::too_many_arguments)] pub(crate) fn new( terminal_surface_id: EntityId, terminal_model: Arc>, @@ -117,6 +119,7 @@ impl TuiHandoffModel { context: ModelHandle, current_working_directory: Option, argument: Option, + team_context_resolver: TeamContextResolver, ctx: &mut AppContext, ) -> Result, TuiHandoffPreparationFailure> { if !AISettings::as_ref(ctx).is_cloud_handoff_enabled(ctx) { @@ -229,6 +232,7 @@ impl TuiHandoffModel { pending: Box::new(pending), }, environments, + team_context_resolver, forked_existing_conversation, next_operation_id: 0, execution_cancellation: None, @@ -601,9 +605,17 @@ impl TuiHandoffModel { ctx.notify(); let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client(); + let team_scope = RequestTeamScope::from_scope(&(self.team_context_resolver)(ctx)); let (cancel, cancellation) = oneshot::channel(); self.execution_cancellation = Some(cancel); - let execution = execute_handoff(*pending, ai_client, Some(cancellation), None, ctx); + let execution = execute_handoff( + *pending, + team_scope, + ai_client, + Some(cancellation), + None, + ctx, + ); ctx.spawn(execution, move |model, outcome, ctx| { if !matches!( model.phase, diff --git a/crates/warp_tui/src/handoff/session.rs b/crates/warp_tui/src/handoff/session.rs index f7e5faae070..ce053a8ae00 100644 --- a/crates/warp_tui/src/handoff/session.rs +++ b/crates/warp_tui/src/handoff/session.rs @@ -5,7 +5,7 @@ //! session-specific outcomes such as restoring input or persisting the //! completed card into the transcript. -use warp::tui_export::{HandoffRestoration, record_static_slash_command_accepted}; +use warp::tui_export::{HandoffRestoration, UserWorkspaces, record_static_slash_command_accepted}; use warpui_core::{AppContext, ViewContext, ViewHandle}; use super::TuiTerminalSessionView; @@ -27,6 +27,7 @@ impl TuiTerminalSessionView { return; } let current_working_directory = self.current_working_directory(ctx); + let team_context_resolver = UserWorkspaces::team_context_resolver(ctx.handle()); let model = match TuiHandoffModel::new( self.terminal_surface_id, self.terminal_model.clone(), @@ -34,6 +35,7 @@ impl TuiTerminalSessionView { self.ai_context_model.clone(), current_working_directory, argument.cloned(), + team_context_resolver, ctx, ) { Ok(model) => model, diff --git a/crates/warp_tui/src/orchestration_model.rs b/crates/warp_tui/src/orchestration_model.rs index d3d14cb5e74..57aa0d3e44e 100644 --- a/crates/warp_tui/src/orchestration_model.rs +++ b/crates/warp_tui/src/orchestration_model.rs @@ -21,14 +21,14 @@ use warp::tui_export::{ BlocklistAIHistoryEvent, BlocklistAIHistoryModel, CloudAgentStartupIssue, CloudConversationData, ConversationStatus, Harness, LoadedSubtreeRollup, OrchestrationEventStreamer, OrchestrationEventStreamerEvent, PreparedRemoteChildLaunch, - RemoteChildLaunchConfig, RenderableAIError, ResolvedTeamScope, ServerApiProvider, - StartAgentExecutionMode, StartAgentRequest, UserWorkspaces, aggregated_orchestrator_status, - apply_child_agent_model_override, child_conversations_in_pill_order, - classify_cloud_agent_startup_error, descendant_conversation_ids_in_spawn_order, - descendant_conversations_in_pill_order, finish_local_oz_child_conversation, - inherit_child_agent_settings, loaded_subtree_rollup, orchestration_root_conversation_id, - oz_run_url, prepare_local_oz_child_launch, prepare_remote_child_launch, - register_agent_event_consumer, unregister_agent_event_consumer, + RemoteChildLaunchConfig, RenderableAIError, RequestTeamScope, ResolvedTeamScope, + ServerApiProvider, StartAgentExecutionMode, StartAgentRequest, UserWorkspaces, + aggregated_orchestrator_status, apply_child_agent_model_override, + child_conversations_in_pill_order, classify_cloud_agent_startup_error, + descendant_conversation_ids_in_spawn_order, descendant_conversations_in_pill_order, + finish_local_oz_child_conversation, inherit_child_agent_settings, loaded_subtree_rollup, + orchestration_root_conversation_id, oz_run_url, prepare_local_oz_child_launch, + prepare_remote_child_launch, register_agent_event_consumer, unregister_agent_event_consumer, }; use warp_core::features::FeatureFlag; use warpui::SingletonEntity; @@ -124,6 +124,7 @@ pub(crate) enum TuiOrchestrationEvent { parent_session_id: TuiSessionId, request: Box, prepared: Box, + team_scope: RequestTeamScope, }, KillLocalChildSession { session_id: TuiSessionId, @@ -493,6 +494,7 @@ impl TuiOrchestrationModel { &mut self, parent_session_id: TuiSessionId, request: StartAgentRequest, + team_scope: RequestTeamScope, working_directory: Option, ctx: &mut ModelContext, ) { @@ -542,6 +544,7 @@ impl TuiOrchestrationModel { self.begin_remote_child_launch( parent_session_id, request, + team_scope, RemoteChildLaunchConfig { environment_id, skill_references, @@ -565,6 +568,7 @@ impl TuiOrchestrationModel { &mut self, parent_session_id: TuiSessionId, request: StartAgentRequest, + team_scope: RequestTeamScope, config: RemoteChildLaunchConfig, ctx: &mut ModelContext, ) { @@ -579,6 +583,7 @@ impl TuiOrchestrationModel { parent_session_id, request: Box::new(request), prepared: Box::new(prepared), + team_scope, }); } @@ -588,6 +593,7 @@ impl TuiOrchestrationModel { child: RemoteChildSession, request: StartAgentRequest, prepared: PreparedRemoteChildLaunch, + team_scope: RequestTeamScope, ctx: &mut ModelContext, ) { let PreparedRemoteChildLaunch { @@ -610,7 +616,7 @@ impl TuiOrchestrationModel { let ai_client = ServerApiProvider::as_ref(ctx).get_ai_client(); let cloud_run_state_for_launch = cloud_run_state.clone(); ctx.spawn( - async move { ai_client.spawn_agent(spawn_request).await }, + async move { ai_client.spawn_agent(spawn_request, team_scope).await }, move |me, result, ctx| { let result = result.map_err(|error| classify_cloud_agent_startup_error(&error)); me.finish_remote_child_launch( diff --git a/crates/warp_tui/src/orchestration_model_tests.rs b/crates/warp_tui/src/orchestration_model_tests.rs index 9744a4570e2..0449cd7d8e3 100644 --- a/crates/warp_tui/src/orchestration_model_tests.rs +++ b/crates/warp_tui/src/orchestration_model_tests.rs @@ -1,9 +1,9 @@ use warp::tui_export::{ AIConversationId, AmbientAgentTaskId, BlocklistAIHistoryModel, CloudAgentStartupBlocker, CloudAgentStartupFailure, CloudAgentStartupIssue, ConversationStatus, Harness, - OrchestrationEventStreamerEvent, RenderableAIError, StartAgentExecutionMode, + OrchestrationEventStreamerEvent, RenderableAIError, RequestTeamScope, StartAgentExecutionMode, StartAgentExecutor, StartAgentExecutorEvent, StartAgentOutcome, StartAgentRequest, - register_tui_session_view_test_singletons, + UserWorkspaces, register_tui_session_view_test_singletons, }; use warp_core::features::FeatureFlag; use warpui::platform::WindowStyle; @@ -180,9 +180,13 @@ fn add_relayed_executor( ctx.subscribe_to_model(&executor, move |_, event, ctx| { orchestration.update(ctx, |orchestration, ctx| match event { StartAgentExecutorEvent::CreateAgent(request) => { + let team_scope = RequestTeamScope::from_scope( + &(UserWorkspaces::teamless_context_resolver_for_test())(ctx), + ); orchestration.dispatch_create_agent( parent_session_id, (**request).clone(), + team_scope, None, ctx, ); diff --git a/crates/warp_tui/src/session_registry.rs b/crates/warp_tui/src/session_registry.rs index 0fbd51eeaef..880d915f401 100644 --- a/crates/warp_tui/src/session_registry.rs +++ b/crates/warp_tui/src/session_registry.rs @@ -11,8 +11,8 @@ use pathfinder_geometry::vector::Vector2F; use warp::tui_export::{ AIConversation, AIConversationAutoexecuteMode, AIConversationId, AmbientAgentTaskId, BannerState, BlocklistAIHistoryModel, GlobalResourceHandlesProvider, IsSharedSessionCreator, - LocalTtyTerminalManager, PersistenceWriter, ServerConversationToken, TerminalManagerTrait, - TerminalSurfaceResult, oz_run_url, + LocalTtyTerminalManager, PersistenceWriter, RequestTeamScope, ServerConversationToken, + TerminalManagerTrait, TerminalSurfaceResult, UserWorkspaces, oz_run_url, }; use warpui::SingletonEntity; use warpui_core::runtime::TuiDriverHandle; @@ -322,15 +322,18 @@ impl TuiSessions { let id = TuiSessionId(view.id()); if ctx.has_singleton_model::() { let orchestration = TuiOrchestrationModel::handle(ctx); + let team_context_resolver = UserWorkspaces::team_context_resolver(view.downgrade()); ctx.subscribe_to_view(&view, move |_, event, ctx| match event { TuiTerminalSessionEvent::StartAgentConversation { request, working_directory, } => { + let team_scope = RequestTeamScope::from_scope(&team_context_resolver(ctx)); orchestration.update(ctx, |orchestration, ctx| { orchestration.dispatch_create_agent( id, (**request).clone(), + team_scope, working_directory.clone(), ctx, ); @@ -465,6 +468,7 @@ impl TuiSessions { parent_session_id, request, prepared, + team_scope, } => { let child = Self::create_remote_child_session(&sessions, *parent_session_id, ctx); orchestration_for_events.update(ctx, |orchestration, ctx| { @@ -472,6 +476,7 @@ impl TuiSessions { child, (**request).clone(), (**prepared).clone(), + *team_scope, ctx, ); });