Add client-side user setting for agent commit attribution (#9323)
This commit is contained in:
@@ -1446,6 +1446,20 @@ define_settings_group!(AISettings, settings: [
|
||||
sync_to_cloud: SyncToCloud::Never,
|
||||
private: true,
|
||||
}
|
||||
|
||||
// Whether Oz should add attribution (co-author line) to commit messages and PRs.
|
||||
// This is the user-level preference; it may be overridden by the team-level
|
||||
// `enable_warp_attribution` AdminEnablementSetting (see
|
||||
// `UserWorkspaces::get_agent_attribution_setting`).
|
||||
agent_attribution_enabled: AgentAttributionEnabled {
|
||||
type: bool,
|
||||
default: true,
|
||||
supported_platforms: SupportedPlatforms::ALL,
|
||||
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::No),
|
||||
private: false,
|
||||
toml_path: "agents.oz.other.agent_attribution_enabled",
|
||||
description: "Whether Oz adds an attribution co-author line to commit messages and pull requests it creates.",
|
||||
}
|
||||
]);
|
||||
|
||||
impl AISettings {
|
||||
|
||||
@@ -121,7 +121,7 @@ use crate::server::telemetry::{
|
||||
};
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::view_components::dropdown::DropdownAction;
|
||||
use crate::workspaces::workspace::CustomerType;
|
||||
use crate::workspaces::workspace::{AdminEnablementSetting, CustomerType};
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
editor::Event as EditorEvent,
|
||||
@@ -148,8 +148,7 @@ const AI_SETTINGS_DROPDOWN_WIDTH: f32 = 250.;
|
||||
const AI_SETTINGS_DROPDOWN_MAX_HEIGHT: f32 = 250.;
|
||||
const NEXT_COMMAND_DESCRIPTION: &str = "Let AI suggest the next command to run based on your command history, outputs, and common workflows.";
|
||||
const PROMPT_SUGGESTIONS_DESCRIPTION: &str = "Let AI suggest natural language prompts, as inline banners in the input, based on recent commands and their outputs.";
|
||||
const SUGGESTED_CODE_BANNERS_DESCRIPTION: &str =
|
||||
"Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs.";
|
||||
const SUGGESTED_CODE_BANNERS_DESCRIPTION: &str = "Let AI suggest code diffs and queries as inline banners in the blocklist, based on recent commands and their outputs.";
|
||||
const NATURAL_LANGUAGE_AUTOSUGGESTIONS: &str =
|
||||
"Let AI suggest natural language autosuggestions, based on recent commands and their outputs.";
|
||||
const SHARED_BLOCK_TITLE_GENERATION_DESCRIPTION: &str =
|
||||
@@ -1470,6 +1469,7 @@ impl AISettingsPageView {
|
||||
widgets.push(Box::new(CLIAgentWidget::default()));
|
||||
widgets.push(Box::new(ApiKeysWidget::new(ctx)));
|
||||
widgets.push(Box::new(AwsBedrockWidget::new(ctx)));
|
||||
widgets.push(Box::new(AgentAttributionWidget::default()));
|
||||
widgets.push(Box::new(OtherAIWidget::default()));
|
||||
if FeatureFlag::AgentModeComputerUse.is_enabled() {
|
||||
widgets.push(Box::new(CloudAgentComputerUseWidget::default()));
|
||||
@@ -1509,6 +1509,7 @@ impl AISettingsPageView {
|
||||
}
|
||||
widgets.push(Box::new(ApiKeysWidget::new(ctx)));
|
||||
widgets.push(Box::new(AwsBedrockWidget::new(ctx)));
|
||||
widgets.push(Box::new(AgentAttributionWidget::default()));
|
||||
widgets.push(Box::new(OtherAIWidget::default()));
|
||||
if FeatureFlag::AgentModeComputerUse.is_enabled() {
|
||||
widgets.push(Box::new(CloudAgentComputerUseWidget::default()));
|
||||
@@ -2112,6 +2113,7 @@ pub enum AISettingsPageAction {
|
||||
ToggleCloudAgentComputerUse,
|
||||
ToggleFileBasedMcp,
|
||||
ToggleIncludeAgentCommandsInHistory,
|
||||
ToggleAgentAttribution,
|
||||
#[cfg(feature = "local_fs")]
|
||||
SetConversationLayout(crate::util::file::external_editor::settings::OpenConversationPreference),
|
||||
ToggleOrchestration,
|
||||
@@ -2269,7 +2271,9 @@ impl TypedActionView for AISettingsPageView {
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to set value for Natural Language Autosuggestions setting: {e:?}");
|
||||
log::warn!(
|
||||
"Failed to set value for Natural Language Autosuggestions setting: {e:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
ctx.notify();
|
||||
@@ -2825,6 +2829,17 @@ impl TypedActionView for AISettingsPageView {
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
AISettingsPageAction::ToggleAgentAttribution => {
|
||||
// The updated value syncs to warp-server automatically via
|
||||
// `CloudPreferencesSyncer` as a `JsonPreference` GSO keyed
|
||||
// `Global_AgentAttributionEnabled`; no bespoke server call needed.
|
||||
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
||||
report_if_error!(settings
|
||||
.agent_attribution_enabled
|
||||
.toggle_and_save_value(ctx));
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4314,10 +4329,13 @@ impl AgentsWidget {
|
||||
render_dropdown_item(
|
||||
appearance,
|
||||
"Base model",
|
||||
Some("This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization."),
|
||||
Some(
|
||||
"This model serves as the primary engine behind the Warp Agent. It powers most interactions and invokes other models for tasks like planning or code generation when necessary. Warp may automatically switch to alternate models based on model availability or for auxiliary tasks such as conversation summarization.",
|
||||
),
|
||||
Some(show_in_prompt_checkbox),
|
||||
LocalOnlyIconState::Hidden,
|
||||
(!ai_settings.is_any_ai_enabled(app)).then(|| appearance.theme().disabled_ui_text_color()),
|
||||
(!ai_settings.is_any_ai_enabled(app))
|
||||
.then(|| appearance.theme().disabled_ui_text_color()),
|
||||
&view.base_model_dropdown,
|
||||
)
|
||||
}
|
||||
@@ -4343,7 +4361,7 @@ impl AgentsWidget {
|
||||
|
||||
let codebase_context_description = vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Allow the Warp Agent to generate an outline of your codebase that can be used for context. No code is ever stored on our servers. "
|
||||
"Allow the Warp Agent to generate an outline of your codebase that can be used for context. No code is ever stored on our servers. ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink(
|
||||
"Learn more",
|
||||
@@ -4415,10 +4433,18 @@ impl AgentsWidget {
|
||||
|
||||
let subtext = {
|
||||
let subtext_fragments = vec![
|
||||
FormattedTextFragment::plain_text("You haven't added any MCP servers yet. Once you do, you'll be able to control how much autonomy the Warp Agent has when interacting with them. "),
|
||||
FormattedTextFragment::hyperlink_action("Add a server", AISettingsPageAction::OpenMCPServerCollection),
|
||||
FormattedTextFragment::plain_text(
|
||||
"You haven't added any MCP servers yet. Once you do, you'll be able to control how much autonomy the Warp Agent has when interacting with them. ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink_action(
|
||||
"Add a server",
|
||||
AISettingsPageAction::OpenMCPServerCollection,
|
||||
),
|
||||
FormattedTextFragment::plain_text(" or "),
|
||||
FormattedTextFragment::hyperlink("learn more about MCPs.", "https://docs.warp.dev/agent-platform/capabilities/mcp"),
|
||||
FormattedTextFragment::hyperlink(
|
||||
"learn more about MCPs.",
|
||||
"https://docs.warp.dev/agent-platform/capabilities/mcp",
|
||||
),
|
||||
];
|
||||
|
||||
Container::new(
|
||||
@@ -4764,11 +4790,16 @@ impl AIInputWidget {
|
||||
Vec<FormattedTextFragment>,
|
||||
> = LazyLock::new(|| {
|
||||
vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Enabling natural language detection will detect when natural language is written in the terminal input, and then automatically switch to Agent Mode for AI queries."
|
||||
),
|
||||
FormattedTextFragment::plain_text(" Encountered an incorrect input detection? "),
|
||||
FormattedTextFragment::hyperlink("Let us know", "https://warpdotdev.typeform.com/to/offrTIpq"),
|
||||
FormattedTextFragment::plain_text(
|
||||
"Enabling natural language detection will detect when natural language is written in the terminal input, and then automatically switch to Agent Mode for AI queries.",
|
||||
),
|
||||
FormattedTextFragment::plain_text(
|
||||
" Encountered an incorrect input detection? ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink(
|
||||
"Let us know",
|
||||
"https://warpdotdev.typeform.com/to/offrTIpq",
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -4871,7 +4902,7 @@ impl SettingsWidget for MCPServersWidget {
|
||||
|
||||
let mcp_description = vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Add MCP servers to extend the Warp Agent's capabilities. \
|
||||
"Add MCP servers to extend the Warp Agent's capabilities. \
|
||||
MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink(
|
||||
@@ -5170,10 +5201,12 @@ impl VoiceWidget {
|
||||
));
|
||||
|
||||
let voice_input_description_text_fragments = vec![
|
||||
FormattedTextFragment::plain_text("Voice input allows you to control Warp by speaking directly to your terminal (powered by "),
|
||||
FormattedTextFragment::hyperlink("Wispr Flow", WISPR_FLOW_URL),
|
||||
FormattedTextFragment::plain_text(")."),
|
||||
];
|
||||
FormattedTextFragment::plain_text(
|
||||
"Voice input allows you to control Warp by speaking directly to your terminal (powered by ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink("Wispr Flow", WISPR_FLOW_URL),
|
||||
FormattedTextFragment::plain_text(")."),
|
||||
];
|
||||
|
||||
let voice_input_description = FormattedTextElement::new(
|
||||
FormattedText::new([FormattedTextLine::Line(
|
||||
@@ -5679,6 +5712,139 @@ impl SettingsWidget for CLIAgentWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// The presentation state of the agent attribution toggle, derived from the
|
||||
/// org-level [`AdminEnablementSetting`], the user's stored preference, and
|
||||
/// whether AI is globally enabled.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct AgentAttributionToggleState {
|
||||
/// Whether the toggle is rendered in the checked state.
|
||||
pub(crate) is_enabled: bool,
|
||||
/// Whether the org has forced the value (locking the toggle with a tooltip).
|
||||
pub(crate) is_forced_by_org: bool,
|
||||
/// Whether the toggle should be rendered as non-interactive overall
|
||||
/// (forced by the org, or AI globally disabled).
|
||||
pub(crate) is_disabled: bool,
|
||||
}
|
||||
|
||||
/// Derive the toggle state from its three inputs.
|
||||
pub(crate) fn derive_agent_attribution_toggle_state(
|
||||
org_setting: &AdminEnablementSetting,
|
||||
user_pref: bool,
|
||||
is_any_ai_enabled: bool,
|
||||
) -> AgentAttributionToggleState {
|
||||
let is_forced_by_org = match org_setting {
|
||||
AdminEnablementSetting::Enable | AdminEnablementSetting::Disable => true,
|
||||
AdminEnablementSetting::RespectUserSetting => false,
|
||||
};
|
||||
let is_enabled = match org_setting {
|
||||
AdminEnablementSetting::Enable => true,
|
||||
AdminEnablementSetting::Disable => false,
|
||||
AdminEnablementSetting::RespectUserSetting => user_pref,
|
||||
};
|
||||
AgentAttributionToggleState {
|
||||
is_enabled,
|
||||
is_forced_by_org,
|
||||
is_disabled: is_forced_by_org || !is_any_ai_enabled,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AgentAttributionWidget {
|
||||
toggle: SwitchStateHandle,
|
||||
}
|
||||
|
||||
impl SettingsWidget for AgentAttributionWidget {
|
||||
type View = AISettingsPageView;
|
||||
|
||||
fn search_terms(&self) -> &str {
|
||||
"agent attribution commit pull request co-author author credit oz warp"
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
_view: &Self::View,
|
||||
appearance: &Appearance,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let ai_settings = AISettings::as_ref(app);
|
||||
let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app);
|
||||
|
||||
let org_setting = UserWorkspaces::as_ref(app).get_agent_attribution_setting();
|
||||
let state = derive_agent_attribution_toggle_state(
|
||||
&org_setting,
|
||||
*ai_settings.agent_attribution_enabled,
|
||||
is_any_ai_enabled,
|
||||
);
|
||||
|
||||
let ui_builder = appearance.ui_builder();
|
||||
let toggle = if state.is_forced_by_org {
|
||||
ui_builder
|
||||
.switch(self.toggle.clone())
|
||||
.check(state.is_enabled)
|
||||
.with_tooltip(TooltipConfig {
|
||||
text: "This option is enforced by your organization's settings and cannot be customized.".to_string(),
|
||||
styles: ui_builder.default_tool_tip_styles(),
|
||||
})
|
||||
.disable()
|
||||
.build()
|
||||
.finish()
|
||||
} else if !is_any_ai_enabled {
|
||||
ui_builder
|
||||
.switch(self.toggle.clone())
|
||||
.check(state.is_enabled)
|
||||
.with_disabled(true)
|
||||
.build()
|
||||
.finish()
|
||||
} else {
|
||||
ui_builder
|
||||
.switch(self.toggle.clone())
|
||||
.check(state.is_enabled)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AISettingsPageAction::ToggleAgentAttribution);
|
||||
})
|
||||
.finish()
|
||||
};
|
||||
|
||||
let toggle_row = build_toggle_element(
|
||||
render_body_item_label::<AISettingsPageAction>(
|
||||
"Enable agent attribution".to_string(),
|
||||
Some(styles::header_font_color(!state.is_disabled, app)),
|
||||
None,
|
||||
LocalOnlyIconState::Hidden,
|
||||
ToggleState::Enabled,
|
||||
appearance,
|
||||
),
|
||||
toggle,
|
||||
appearance,
|
||||
None,
|
||||
);
|
||||
|
||||
Flex::column()
|
||||
.with_child(render_separator(appearance))
|
||||
.with_child(
|
||||
build_sub_header(
|
||||
appearance,
|
||||
"Agent Attribution",
|
||||
Some(styles::header_font_color(is_any_ai_enabled, app)),
|
||||
)
|
||||
.with_padding_bottom(HEADER_PADDING)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(toggle_row)
|
||||
.with_child(render_ai_setting_description(
|
||||
"Oz can add attribution to commit messages and pull requests it creates",
|
||||
!state.is_disabled,
|
||||
app,
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "ai_page_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[derive(Default)]
|
||||
struct CloudAgentComputerUseWidget {
|
||||
toggle: SwitchStateHandle,
|
||||
@@ -6028,11 +6194,9 @@ impl ApiKeysWidget {
|
||||
FormattedTextFragment::plain_text(" to use your own API keys."),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Ask your team's admin to upgrade to the Build plan to use your own API keys.",
|
||||
),
|
||||
]
|
||||
vec![FormattedTextFragment::plain_text(
|
||||
"Ask your team's admin to upgrade to the Build plan to use your own API keys.",
|
||||
)]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use super::{derive_agent_attribution_toggle_state, AgentAttributionToggleState};
|
||||
use crate::workspaces::workspace::AdminEnablementSetting;
|
||||
|
||||
#[test]
|
||||
fn respect_user_setting_returns_user_pref_unlocked() {
|
||||
let state = derive_agent_attribution_toggle_state(
|
||||
&AdminEnablementSetting::RespectUserSetting,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
state,
|
||||
AgentAttributionToggleState {
|
||||
is_enabled: true,
|
||||
is_forced_by_org: false,
|
||||
is_disabled: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respect_user_setting_with_user_off_returns_unchecked_unlocked() {
|
||||
let state = derive_agent_attribution_toggle_state(
|
||||
&AdminEnablementSetting::RespectUserSetting,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
state,
|
||||
AgentAttributionToggleState {
|
||||
is_enabled: false,
|
||||
is_forced_by_org: false,
|
||||
is_disabled: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_enable_locks_toggle_on_regardless_of_user_pref() {
|
||||
let state = derive_agent_attribution_toggle_state(&AdminEnablementSetting::Enable, false, true);
|
||||
assert_eq!(
|
||||
state,
|
||||
AgentAttributionToggleState {
|
||||
is_enabled: true,
|
||||
is_forced_by_org: true,
|
||||
is_disabled: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_disable_locks_toggle_off_regardless_of_user_pref() {
|
||||
let state = derive_agent_attribution_toggle_state(&AdminEnablementSetting::Disable, true, true);
|
||||
assert_eq!(
|
||||
state,
|
||||
AgentAttributionToggleState {
|
||||
is_enabled: false,
|
||||
is_forced_by_org: true,
|
||||
is_disabled: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ai_globally_disabled_marks_toggle_disabled_but_not_forced() {
|
||||
let state = derive_agent_attribution_toggle_state(
|
||||
&AdminEnablementSetting::RespectUserSetting,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
assert_eq!(
|
||||
state,
|
||||
AgentAttributionToggleState {
|
||||
is_enabled: true,
|
||||
is_forced_by_org: false,
|
||||
is_disabled: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn team_force_takes_precedence_over_global_ai_disabled() {
|
||||
let state =
|
||||
derive_agent_attribution_toggle_state(&AdminEnablementSetting::Enable, false, false);
|
||||
assert_eq!(
|
||||
state,
|
||||
AgentAttributionToggleState {
|
||||
is_enabled: true,
|
||||
is_forced_by_org: true,
|
||||
is_disabled: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -827,6 +827,10 @@ impl From<GqlWorkspaceSettings> for WorkspaceSettings {
|
||||
.map(|denylist| denylist.to_predicates()),
|
||||
}
|
||||
}),
|
||||
enable_warp_attribution: gql_workspace_settings
|
||||
.ambient_agent_settings
|
||||
.map(|s| s.enable_warp_attribution.into())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1456,6 +1456,16 @@ impl UserWorkspaces {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the team-level agent attribution setting.
|
||||
///
|
||||
/// Use this to decide whether the user's attribution toggle should be locked
|
||||
/// (`Enable`/`Disable`) or editable (`RespectUserSetting`).
|
||||
pub fn get_agent_attribution_setting(&self) -> AdminEnablementSetting {
|
||||
self.current_team()
|
||||
.map(|team| team.organization_settings.enable_warp_attribution.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns only the organization-specific codebase context enablement setting.
|
||||
/// Do not use this function to determine whether codebase context is generally enabled --
|
||||
/// use `is_codebase_context_enabled` instead.
|
||||
|
||||
@@ -614,6 +614,108 @@ fn test_joining_team_moves_objects() {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_attribution_default_with_no_workspace() {
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources { workspaces: vec![] },
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
let setting = UserWorkspaces::as_ref(ctx).get_agent_attribution_setting();
|
||||
assert_eq!(
|
||||
setting,
|
||||
AdminEnablementSetting::RespectUserSetting,
|
||||
"attribution should default to RespectUserSetting when there is no workspace"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_attribution_forced_on_by_team() {
|
||||
let mut team = team_for_test();
|
||||
team.organization_settings.enable_warp_attribution = AdminEnablementSetting::Enable;
|
||||
let workspace = workspace_for_test(&team);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
let setting = UserWorkspaces::as_ref(ctx).get_agent_attribution_setting();
|
||||
assert_eq!(
|
||||
setting,
|
||||
AdminEnablementSetting::Enable,
|
||||
"attribution should be Enable when forced on by the team"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_attribution_forced_off_by_team() {
|
||||
let mut team = team_for_test();
|
||||
team.organization_settings.enable_warp_attribution = AdminEnablementSetting::Disable;
|
||||
let workspace = workspace_for_test(&team);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
let setting = UserWorkspaces::as_ref(ctx).get_agent_attribution_setting();
|
||||
assert_eq!(
|
||||
setting,
|
||||
AdminEnablementSetting::Disable,
|
||||
"attribution should be Disable when forced off by the team"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_agent_attribution_respects_user_setting() {
|
||||
let mut team = team_for_test();
|
||||
team.organization_settings.enable_warp_attribution = AdminEnablementSetting::RespectUserSetting;
|
||||
let workspace = workspace_for_test(&team);
|
||||
|
||||
App::test((), |mut app| async move {
|
||||
initialize_app(
|
||||
&mut app,
|
||||
CachedResources {
|
||||
workspaces: vec![workspace],
|
||||
},
|
||||
Arc::new(MockTeamClient::new()),
|
||||
Arc::new(MockWorkspaceClient::new()),
|
||||
);
|
||||
|
||||
app.read(|ctx| {
|
||||
let setting = UserWorkspaces::as_ref(ctx).get_agent_attribution_setting();
|
||||
assert_eq!(
|
||||
setting,
|
||||
AdminEnablementSetting::RespectUserSetting,
|
||||
"attribution should be RespectUserSetting when the team defers to user preference"
|
||||
);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leaving_team_moves_objects() {
|
||||
let _flag = FeatureFlag::SharedWithMe.override_enabled(true);
|
||||
|
||||
@@ -774,4 +774,8 @@ pub struct WorkspaceSettings {
|
||||
pub addon_credits_settings: AddonCreditsSettings,
|
||||
pub codebase_context_settings: CodebaseContextSettings,
|
||||
pub sandboxed_agent_settings: Option<SandboxedAgentSettings>,
|
||||
/// The team-level agent attribution setting. When `Enable` or `Disable`, the
|
||||
/// user toggle is locked. When `RespectUserSetting` (or absent), the user can choose.
|
||||
#[serde(default)]
|
||||
pub enable_warp_attribution: AdminEnablementSetting,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user