@@ -39,10 +39,10 @@ use crate::settings::{
3939 AISettings , AISettingsChangedEvent , CodeSettings , CodeSettingsChangedEvent , PrivacySettings ,
4040} ;
4141#[ cfg( test) ]
42- use crate :: workspaces:: workspace:: { AIAutonomyPolicy , WorkspaceMember , WorkspaceSettings } ;
42+ use crate :: workspaces:: workspace:: { AIAutonomyPolicy , WorkspaceMember } ;
4343use crate :: workspaces:: workspace:: {
4444 AiAutonomySettings , AiOverages , PurchaseAddOnCreditsPolicy , SandboxedAgentSettings ,
45- UsageBasedPricingSettings ,
45+ UsageBasedPricingSettings , WorkspaceSettings ,
4646} ;
4747
4848const STRIPE_SUBSCRIPTION_INTERVAL_PAGE_PREFIX : & str = "/upgrade" ;
@@ -185,10 +185,6 @@ pub struct CreateTeamResponse {
185185/// [`TeamScope`]'s contract. Code with no window at all (e.g. background GEAP token refresh)
186186/// is not this type's job -- it needs its own accessor that reads across every one of the
187187/// user's teams explicitly, in the shape of `UserWorkspaces::teams_allow_codebase_context`.
188- // Nothing constructs or consumes one outside this module's own tests yet; remove this
189- // `#[allow(dead_code)]`, and widen visibility to `pub`, once a Group 1 migration PR has a real
190- // call site.
191- #[ allow( dead_code) ]
192188pub ( crate ) struct TeamContextForOperation {
193189 team_uid : Option < ServerId > ,
194190}
@@ -208,9 +204,6 @@ pub(crate) struct TeamContextForOperation {
208204/// workspace-level data. Code with no window at all must not construct a scope to route around
209205/// this; it should read across every team explicitly, the way
210206/// `UserWorkspaces::teams_allow_codebase_context` does.
211- // Only tests call `team_uid()` today; remove this `#[allow(dead_code)]` once a Group 1
212- // migration PR has a real getter generic over this trait.
213- #[ allow( dead_code) ]
214207pub ( crate ) trait TeamScope {
215208 fn team_uid ( & self ) -> Option < ServerId > ;
216209}
@@ -241,9 +234,6 @@ impl TeamContextForOperation {
241234/// a [`TeamContextForOperation`]. A [`WeakViewHandle`] locates a window to read from; it is
242235/// not evidence that the holder is running in that window, which is what minting operation
243236/// scope requires.
244- // Only tests construct one today; remove this once a Group 1 migration PR resolves one from a
245- // real render.
246- #[ allow( dead_code) ]
247237pub ( crate ) struct TeamContext < ' a > {
248238 team_uid : Option < & ' a ServerId > ,
249239}
@@ -451,8 +441,6 @@ impl UserWorkspaces {
451441 /// [`TeamContextForOperation`]. This is the only way application code mints one. Always
452442 /// succeeds -- a window with no team selected still yields a scope, just one whose
453443 /// `team_uid()` is `None`; see [`TeamScope`]'s contract for what that means to a getter.
454- // Only tests call this today; remove once a Group 1 migration PR has a real call site.
455- #[ allow( dead_code) ]
456444 pub ( crate ) fn team_context_for_operation < T : Entity > (
457445 & self ,
458446 ctx : & ViewContext < T > ,
@@ -463,8 +451,6 @@ impl UserWorkspaces {
463451 }
464452
465453 /// Resolves `view`'s window team for one render. See [`TeamContext`].
466- // Only tests call this today; remove once a Group 1 migration PR has a real call site.
467- #[ allow( dead_code) ]
468454 pub ( crate ) fn team_context < ' a , T : Entity > (
469455 & ' a self ,
470456 view : & WeakViewHandle < T > ,
@@ -1974,19 +1960,107 @@ impl UserWorkspaces {
19741960 }
19751961 }
19761962
1977- pub fn default_host_slug ( & self ) -> Option < & str > {
1963+ /// The current workspace's settings, but only when the user belongs to no team at all.
1964+ ///
1965+ /// `WorkspaceSettings` is not team-neutral data. Whenever the user has any team,
1966+ /// `GetEffectiveWorkspaceSettingsForWorkspace` resolves one arbitrarily-chosen team
1967+ /// server-side and falls through to a literal `workspaceTeamIDs[0]`, so reading it as a
1968+ /// default hands back some other team's policy. It is trustworthy only for a genuinely
1969+ /// teamless user, whose settings the server computes from tier defaults. This applies the
1970+ /// same guard as [`Self::teams_allow_codebase_context`], which is the shape scoped getters
1971+ /// reuse for their no-team branch.
1972+ fn teamless_workspace_settings ( & self ) -> Option < & WorkspaceSettings > {
1973+ let is_on_a_team = self
1974+ . workspaces
1975+ . iter ( )
1976+ . any ( |workspace| !workspace. teams . is_empty ( ) ) ;
1977+ if is_on_a_team {
1978+ return None ;
1979+ }
19781980 self . current_workspace ( )
1979- . and_then ( |workspace| workspace. settings . default_host_slug . as_deref ( ) )
1981+ . map ( |workspace| & workspace. settings )
19801982 }
19811983
1982- /// Returns the team-level agent attribution setting .
1984+ /// The default self-hosted worker host slug configured for `scope`'s team .
19831985 ///
1984- /// Use this to decide whether the user's attribution toggle should be locked
1985- /// (`Enable`/`Disable`) or editable (`RespectUserSetting`).
1986- pub fn get_agent_attribution_setting ( & self ) -> AdminEnablementSetting {
1986+ /// Returns `None` when that team configures none, and also when the scope has no team
1987+ /// while the user is on some other team: another team's host is not a substitute. See
1988+ /// [`TeamScope`].
1989+ pub ( crate ) fn default_host_slug_for_scope ( & self , scope : & impl TeamScope ) -> Option < & str > {
1990+ match scope. team_uid ( ) {
1991+ Some ( team_uid) => self
1992+ . team_from_uid ( team_uid)
1993+ . and_then ( |team| team. settings . default_host_slug . as_deref ( ) ) ,
1994+ None => self
1995+ . teamless_workspace_settings ( )
1996+ . and_then ( |settings| settings. default_host_slug . as_deref ( ) ) ,
1997+ }
1998+ }
1999+
2000+ /// Whether *some* team the user belongs to configures a default self-hosted worker host.
2001+ ///
2002+ /// This answers only the availability question a windowless surface can honestly ask: the
2003+ /// `/host` slash command is worth offering when a default host exists anywhere. It
2004+ /// deliberately does not choose *which* slug, because there is no defensible ordering over
2005+ /// host slugs the way [`AdminEnablementSetting`] has a most-restrictive direction — with
2006+ /// two teams configuring different hosts, any pick is arbitrary. Windowed callers must use
2007+ /// [`Self::default_host_slug_for_scope`] instead; picking a slug without a window is a
2008+ /// product decision that has not been made.
2009+ ///
2010+ /// Falls back to workspace settings only when the user is on no team, mirroring
2011+ /// [`Self::teams_allow_codebase_context`]'s empty-iterator guard.
2012+ pub fn any_team_has_default_host_slug ( & self ) -> bool {
2013+ let mut team_slugs = self
2014+ . workspaces
2015+ . iter ( )
2016+ . flat_map ( |workspace| workspace. teams . iter ( ) )
2017+ . map ( |team| & team. settings . default_host_slug )
2018+ . peekable ( ) ;
2019+
2020+ if team_slugs. peek ( ) . is_none ( ) {
2021+ return self
2022+ . teamless_workspace_settings ( )
2023+ . is_some_and ( |settings| settings. default_host_slug . is_some ( ) ) ;
2024+ }
2025+
2026+ team_slugs. any ( Option :: is_some)
2027+ }
2028+
2029+ /// The default self-hosted worker host slug from the current workspace's settings.
2030+ ///
2031+ /// **Not a team-neutral read**, despite reading workspace settings: see
2032+ /// [`Self::teamless_workspace_settings`] for why. Sole remaining caller is
2033+ /// `ai::orchestration::resolve_default_host_slug`, which feeds the plan card, the
2034+ /// confirmation card, the TUI orchestration block and the handoff pipeline. That chain
2035+ /// still needs both a windowless accessor the TUI can reach and a pinned scope for the
2036+ /// handoff's chosen destination, so it moves as one follow-up rather than piecemeal.
2037+ /// Do not add callers: windowed code uses [`Self::default_host_slug_for_scope`], and a
2038+ /// windowless availability check uses [`Self::any_team_has_default_host_slug`].
2039+ pub fn unscoped_default_host_slug ( & self ) -> Option < & str > {
19872040 self . current_workspace ( )
1988- . map ( |workspace| workspace. settings . enable_warp_attribution . clone ( ) )
1989- . unwrap_or_default ( )
2041+ . and_then ( |workspace| workspace. settings . default_host_slug . as_deref ( ) )
2042+ }
2043+
2044+ /// The agent attribution policy for `scope`'s team: `Enable` or `Disable` lock the user's
2045+ /// attribution toggle, `RespectUserSetting` leaves it editable.
2046+ ///
2047+ /// This is live UI state rather than a recorded fact, so resolve it from the rendering
2048+ /// window's [`TeamContext`] on each frame; a value captured when a surface opened goes
2049+ /// stale the moment that window switches team.
2050+ pub ( crate ) fn agent_attribution_setting_for_scope (
2051+ & self ,
2052+ scope : & impl TeamScope ,
2053+ ) -> AdminEnablementSetting {
2054+ match scope. team_uid ( ) {
2055+ Some ( team_uid) => self
2056+ . team_from_uid ( team_uid)
2057+ . map ( |team| team. settings . enable_warp_attribution . clone ( ) )
2058+ . unwrap_or_default ( ) ,
2059+ None => self
2060+ . teamless_workspace_settings ( )
2061+ . map ( |settings| settings. enable_warp_attribution . clone ( ) )
2062+ . unwrap_or_default ( ) ,
2063+ }
19902064 }
19912065
19922066 pub fn teams_allow_codebase_context ( & self ) -> AdminEnablementSetting {
0 commit comments