Skip to content

Commit 009972e

Browse files
committed
wip
1 parent 3639ae0 commit 009972e

29 files changed

Lines changed: 305 additions & 261 deletions

app/src/ai/agent_sdk/common.rs

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ use crate::ai::agent_sdk::driver::{AgentDriverError, WARP_DRIVE_SYNC_TIMEOUT};
1717
use crate::ai::ambient_agents::AmbientAgentTaskId;
1818
use crate::ai::cloud_environments::CloudAmbientAgentEnvironment;
1919
use crate::ai::llms::{LLMId, LLMPreferences};
20+
use crate::auth::UserUid;
2021
use crate::auth::auth_state::AuthStateProvider;
2122
use crate::cloud_object::{CloudObject, CloudObjectLookup as _, Owner};
2223
use crate::server::cloud_objects::update_manager::UpdateManager;
2324
use crate::server::ids::{ServerId, SyncId};
2425
use crate::server::server_api::ServerApiProvider;
2526
use crate::server::server_api::ai::AIClient;
2627
use crate::workspaces::update_manager::TeamUpdateManager;
27-
use crate::workspaces::user_workspaces::UserWorkspaces;
28+
use crate::workspaces::user_workspaces::{SoleTeamError, UserWorkspaces};
2829

2930
/// How long to wait for workspace metadata to refresh.
3031
pub const WORKSPACE_METADATA_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
@@ -95,38 +96,56 @@ pub(super) fn set_ambient_task_context_from_run_id(
9596
Ok(task_id)
9697
}
9798

98-
/// Resolve the owner of a new cloud object. This resolution is based on the CLI `--team` and `--personal` flags.
99+
pub(super) fn describe_sole_team_error(error: SoleTeamError) -> anyhow::Error {
100+
match error {
101+
SoleTeamError::NoTeam => anyhow::anyhow!("You are not on a team"),
102+
SoleTeamError::MoreThanOneTeam { team_uids } => anyhow::anyhow!(
103+
"You are on {} teams; specify one with --team <UID>: {}",
104+
team_uids.len(),
105+
team_uids
106+
.iter()
107+
.map(ServerId::to_string)
108+
.collect::<Vec<_>>()
109+
.join(", ")
110+
),
111+
}
112+
}
113+
114+
fn current_user_uid(ctx: &AppContext) -> anyhow::Result<UserUid> {
115+
AuthStateProvider::as_ref(ctx)
116+
.get()
117+
.user_id()
118+
.ok_or_else(|| anyhow::anyhow!("User should be logged in"))
119+
}
120+
121+
/// Resolve the owner of a new cloud object, based on the CLI `--team` and `--personal` flags.
99122
///
100-
/// If `team_flag` is true, attempts to get the current team UID (errors if not on a team).
101-
/// If `user_flag` is true, gets the current user's UID.
102-
/// Otherwise, defaults to team if available, falling back to user.
123+
/// With neither flag, a user on exactly one team gets a team object and a user on no team gets
124+
/// a personal one. A user on several teams is asked to choose rather than silently handed a
125+
/// personal object.
103126
pub fn resolve_owner(team_flag: bool, user_flag: bool, ctx: &AppContext) -> anyhow::Result<Owner> {
104127
if team_flag {
105-
let team_id = UserWorkspaces::as_ref(ctx)
128+
let team_uid = UserWorkspaces::as_ref(ctx)
106129
.sole_team_uid()
107-
.ok_or_else(|| anyhow::anyhow!("User is not on a team"))?;
108-
return Ok(Owner::Team { team_uid: team_id });
130+
.map_err(describe_sole_team_error)?;
131+
return Ok(Owner::Team { team_uid });
109132
}
110133

111134
if user_flag {
112-
let user_id = AuthStateProvider::as_ref(ctx)
113-
.get()
114-
.user_id()
115-
.ok_or_else(|| anyhow::anyhow!("User should be logged in"))?;
116-
return Ok(Owner::User { user_uid: user_id });
135+
return Ok(Owner::User {
136+
user_uid: current_user_uid(ctx)?,
137+
});
117138
}
118139

119-
// Default: try team first, fall back to user
120-
if let Some(team_uid) = UserWorkspaces::as_ref(ctx).sole_team_uid() {
121-
return Ok(Owner::Team { team_uid });
140+
match UserWorkspaces::as_ref(ctx).sole_team_uid() {
141+
Ok(team_uid) => Ok(Owner::Team { team_uid }),
142+
Err(SoleTeamError::NoTeam) => Ok(Owner::User {
143+
user_uid: current_user_uid(ctx)?,
144+
}),
145+
Err(error @ SoleTeamError::MoreThanOneTeam { .. }) => {
146+
Err(describe_sole_team_error(error))
147+
}
122148
}
123-
124-
log::warn!("Tried to default to creating team object, team could not be found.");
125-
let user_id = AuthStateProvider::as_ref(ctx)
126-
.get()
127-
.user_id()
128-
.ok_or_else(|| anyhow::anyhow!("User should be logged in"))?;
129-
Ok(Owner::User { user_uid: user_id })
130149
}
131150

132151
/// Refresh workspace metadata before executing an operation.

app/src/ai/agent_sdk/provider.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use warp_core::channel::ChannelState;
77
use warpui::platform::TerminationMode;
88
use warpui::{AppContext, ModelContext, SingletonEntity};
99

10+
use crate::ai::agent_sdk::common::describe_sole_team_error;
1011
use crate::ai::agent_sdk::output::{self, TableFormat};
1112
use crate::workspaces::user_workspaces::UserWorkspaces;
1213

@@ -58,12 +59,9 @@ impl ProviderCommandRunner {
5859
// TODO(bens): initiate the OAuth flow and use the login-less auth URL
5960
let slug = provider_type.slug();
6061
let url = if use_team_auth {
61-
let team_uid = match UserWorkspaces::as_ref(ctx).sole_team_uid() {
62-
Some(uid) => uid,
63-
None => {
64-
return Err(anyhow::anyhow!("User is not on a team"));
65-
}
66-
};
62+
let team_uid = UserWorkspaces::as_ref(ctx)
63+
.sole_team_uid()
64+
.map_err(describe_sole_team_error)?;
6765
format!("{server_url}/oauth/connect/{slug}?principalType=team&principalId={team_uid}")
6866
} else {
6967
format!("{server_url}/oauth/connect/{slug}")

app/src/ai/blocklist/inline_action/orchestration_controls.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use crate::view_components::FilterableDropdown;
5252
use crate::view_components::dropdown::{
5353
Dropdown, DropdownAction, DropdownItemAction, DropdownStyle,
5454
};
55+
use crate::workspaces::user_workspaces::UserWorkspaces;
5556

5657
// ── Shared constants ────────────────────────────────────────────────
5758

@@ -279,13 +280,15 @@ fn oz_model_menu_items<A: OrchestrationControlAction, V: View>(
279280
.find(|llm| llm.id.to_string() == row.id)
280281
})
281282
.collect();
283+
let scope = UserWorkspaces::as_ref(ctx).team_context_for_view(ctx);
282284
available_model_menu_items(
283285
ordered_choices,
284286
move |llm| DropdownAction::select_action_and_close(A::model_changed(llm.id.to_string())),
285287
None,
286288
None,
287289
false,
288290
false,
291+
&scope,
289292
ctx,
290293
)
291294
}

app/src/ai/blocklist/prompt/prompt_alert.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ impl View for PromptAlertView {
436436
});
437437

438438
let can_purchase_addon_credits = workspaces
439-
.purchase_policy_for_team(current_team)
439+
.purchase_policy()
440440
.is_some_and(|policy| policy.allows_purchases());
441441

442442
let suggest_buy_credits = can_purchase_addon_credits

app/src/ai/custom_model_router_editor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use crate::view_components::action_button::{
4242
ActionButton, ButtonSize, PrimaryTheme, SecondaryTheme,
4343
};
4444
use crate::view_components::dropdown::DropdownAction;
45+
use crate::workspaces::user_workspaces::UserWorkspaces;
4546

4647
pub const HEADER_TEXT: &str = "Router Editor";
4748

@@ -1048,6 +1049,7 @@ fn fill_filterable_dropdown<F>(
10481049
// `set_filtered_items` keeps an empty selection blank rather than
10491050
// auto-selecting the first model.
10501051
dropdown.set_placeholder(MODEL_PLACEHOLDER, ctx);
1052+
let scope = UserWorkspaces::as_ref(ctx).team_context_for_view(ctx);
10511053
let items = available_model_menu_items(
10521054
LLMPreferences::as_ref(ctx)
10531055
.get_base_llm_choices_for_agent_mode(ctx)
@@ -1058,6 +1060,7 @@ fn fill_filterable_dropdown<F>(
10581060
None,
10591061
false,
10601062
false,
1063+
&scope,
10611064
ctx,
10621065
);
10631066
dropdown.set_rich_items(items, ctx);

app/src/ai/document/ai_document_model.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use crate::terminal::TerminalView;
4747
use crate::terminal::model::session::Session;
4848
use crate::terminal::model::session::active_session::ActiveSession;
4949
use crate::throttle::throttle;
50-
use crate::workspaces::user_workspaces::UserWorkspaces;
50+
use crate::workspaces::user_workspaces::{SoleTeamError, UserWorkspaces};
5151

5252
/// The frequency at which we check for modifications and save the AI document to the server.
5353
/// Uses the same 2-second period as notebooks for consistency.
@@ -1275,13 +1275,22 @@ impl AIDocumentModel {
12751275
fn get_plan_owner(ctx: &AppContext) -> Option<Owner> {
12761276
let is_service_account = AuthStateProvider::as_ref(ctx).get().is_service_account();
12771277

1278-
if is_service_account {
1279-
// If the SA doesn't have a team, we'll skip the plan sync in the caller
1280-
UserWorkspaces::as_ref(ctx)
1281-
.sole_team_uid()
1282-
.map(|team_uid| Owner::Team { team_uid })
1283-
} else {
1284-
UserWorkspaces::as_ref(ctx).personal_drive(ctx)
1278+
if !is_service_account {
1279+
return UserWorkspaces::as_ref(ctx).personal_drive(ctx);
1280+
}
1281+
match UserWorkspaces::as_ref(ctx).sole_team_uid() {
1282+
Ok(team_uid) => Some(Owner::Team { team_uid }),
1283+
// The caller skips plan sync without a team.
1284+
Err(SoleTeamError::NoTeam) => None,
1285+
// A service account is bound to exactly one team server-side, so several here means
1286+
// the client's view of its memberships disagrees with that.
1287+
Err(error @ SoleTeamError::MoreThanOneTeam { .. }) => {
1288+
report_error!(
1289+
anyhow::Error::new(error)
1290+
.context("Service account resolved to more than one team")
1291+
);
1292+
None
1293+
}
12851294
}
12861295
}
12871296

app/src/ai/execution_profiles/editor/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,13 +1202,15 @@ impl ExecutionProfileEditorView {
12021202
.iter()
12031203
.any(|llm| matches!(llm.disable_reason, Some(DisableReason::RequiresUpgrade)));
12041204

1205+
let scope = UserWorkspaces::as_ref(ctx).team_context_for_view(ctx);
12051206
let items = available_model_menu_items(
12061207
choices,
12071208
|llm| DropdownAction::select_action_and_close(create_action(llm.id.clone())),
12081209
None,
12091210
None,
12101211
false,
12111212
false,
1213+
&scope,
12121214
ctx,
12131215
);
12141216
dropdown.set_rich_items(items, ctx);
@@ -1250,6 +1252,7 @@ impl ExecutionProfileEditorView {
12501252
.get_coding_llm_choices(ctx)
12511253
.collect_vec();
12521254

1255+
let scope = UserWorkspaces::as_ref(ctx).team_context_for_view(ctx);
12531256
let items = available_model_menu_items(
12541257
choices,
12551258
|llm| {
@@ -1261,6 +1264,7 @@ impl ExecutionProfileEditorView {
12611264
None,
12621265
false,
12631266
false,
1267+
&scope,
12641268
ctx,
12651269
);
12661270
dropdown.set_rich_items(items, ctx);

app/src/ai/execution_profiles/model_menu_items.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::ai::llms::{
1616
should_show_gemini_enterprise_agent_platform_icon_for_model, should_show_key_icon_for_model,
1717
};
1818
use crate::menu::{MenuItem, MenuItemFields, MenuTooltipPosition};
19+
use crate::workspaces::user_workspaces::TeamScope;
1920

2021
pub fn is_auto(llm: &LLMInfo) -> bool {
2122
llm.display_name.to_lowercase().contains("auto")
@@ -75,6 +76,7 @@ fn make_item_fields<A: Action + Clone>(
7576
model_id_to_add_profile_default_label_to: Option<&LLMId>,
7677
collapse_auto: bool,
7778
collapse_reasoning_variants: bool,
79+
scope: &dyn TeamScope,
7880
app: &AppContext,
7981
) -> MenuItem<A> {
8082
let is_auto_model = is_auto(llm);
@@ -88,7 +90,7 @@ fn make_item_fields<A: Action + Clone>(
8890
let is_using_bedrock = should_show_bedrock_icon_for_model(llm, app);
8991
let is_using_gemini_enterprise_agent_platform =
9092
should_show_gemini_enterprise_agent_platform_icon_for_model(llm, app);
91-
let is_using_api_key = should_show_key_icon_for_model(llm, app);
93+
let is_using_api_key = should_show_key_icon_for_model(llm, scope, app);
9294
let is_custom_router = is_custom_router_id(llm.id.as_str());
9395
let leading_icon = model_leading_icon(
9496
llm,
@@ -183,6 +185,7 @@ pub fn available_model_menu_items<A: Action + Clone>(
183185
position_id_fn: Option<&dyn Fn(&LLMId) -> String>,
184186
collapse_auto: bool,
185187
collapse_reasoning_variants: bool,
188+
scope: &dyn TeamScope,
186189
app: &AppContext,
187190
) -> Vec<MenuItem<A>> {
188191
choices
@@ -195,6 +198,7 @@ pub fn available_model_menu_items<A: Action + Clone>(
195198
model_id_to_add_profile_default_label_to,
196199
collapse_auto,
197200
collapse_reasoning_variants,
201+
scope,
198202
app,
199203
)
200204
})

0 commit comments

Comments
 (0)