Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
//! Logic to convert from / to [`ServerExperiment`].
use std::fmt::{Display, Formatter};
use anyhow::{Ok, Result};
use warp_graphql::experiment::Experiment;
use super::ServerExperiment;
impl Display for ServerExperiment {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let str = match self {
Self::SessionSharingControl => "SESSION_SHARING_CONTROL",
Self::SessionSharingExperiment => "SESSION_SHARING_EXPERIMENT",
Self::DisableAgentModeExperiment => "DISABLE_AGENT_MODE_EXPERIMENT",
Self::EnvVarsEarlyAccessExperiment => "ENV_VARS_EARLY_ACCESS_EXPERIMENT",
Self::AgentModeAnalyticsExperiment => "AGENT_MODE_ANALYTICS_EXPERIMENT",
Self::WindowsLaunchExperiment => "WINDOWS_LAUNCH_EXPERIMENT",
Self::TmuxSshWarpificationControl => "TMUX_SSH_WARPIFICATION_CONTROL",
Self::TmuxSshWarpificationExperiment => "TMUX_SSH_WARPIFICATION_EXPERIMENT",
Self::CodebaseContextControl => "CODEBASE_CONTEXT_CONTROL",
Self::CodebaseContextExperiment => "CODEBASE_CONTEXT_EXPERIMENT",
Self::SuggestedCodeDiffsControl => "SUGGESTED_CODE_DIFFS_CONTROL",
Self::SuggestedCodeDiffsExperiment => "SUGGESTED_CODE_DIFFS_EXPERIMENT",
Self::BuildPlanAutoReloadControl => "BUILD_PLAN_AUTO_RELOAD_CONTROL",
Self::BuildPlanAutoReloadBannerToggle => "BUILD_PLAN_AUTO_RELOAD_BANNER_TOGGLE",
Self::BuildPlanAutoReloadPostPurchaseModal => {
"BUILD_PLAN_AUTO_RELOAD_POST_PURCHASE_MODAL"
}
Self::PromptSuggestionsViaMaaControl => "PROMPT_SUGGESTIONS_VIA_MAA_CONTROL",
Self::PromptSuggestionsViaMaaExperiment => "PROMPT_SUGGESTIONS_VIA_MAA_EXPERIMENT",
Self::PromptSuggestionsViaMaaOutOfBandExperiment => {
"PROMPT_SUGGESTIONS_VIA_MAA_OOB_EXPERIMENT"
}
Self::FreeUserNoAiControl => "FREE_USER_NO_AI_CONTROL",
Self::FreeUserNoAiExperiment => "FREE_USER_NO_AI_EXPERIMENT",
Self::OzMultiHarnessControl => "OZ_MULTI_HARNESS_CONTROL",
Self::OzMultiHarnessExperiment => "OZ_MULTI_HARNESS_EXPERIMENT",
#[cfg(test)]
Self::TestExperiment => "TEST_EXPERIMENT",
};
write!(f, "{str}")
}
}
impl ServerExperiment {
pub fn from_string(s: String) -> Result<Self> {
match s.as_str() {
"SESSION_SHARING_CONTROL" => Ok(Self::SessionSharingControl),
"SESSION_SHARING_EXPERIMENT" => Ok(Self::SessionSharingExperiment),
"DISABLE_AGENT_MODE_EXPERIMENT" => Ok(Self::DisableAgentModeExperiment),
"ENV_VARS_EARLY_ACCESS_EXPERIMENT" => Ok(Self::EnvVarsEarlyAccessExperiment),
"AGENT_MODE_ANALYTICS_EXPERIMENT" => Ok(Self::AgentModeAnalyticsExperiment),
"WINDOWS_LAUNCH_EXPERIMENT" => Ok(Self::WindowsLaunchExperiment),
"TMUX_SSH_WARPIFICATION_CONTROL" => Ok(Self::TmuxSshWarpificationControl),
"TMUX_SSH_WARPIFICATION_EXPERIMENT" => Ok(Self::TmuxSshWarpificationExperiment),
"CODEBASE_CONTEXT_EXPERIMENT" => Ok(Self::CodebaseContextExperiment),
"CODEBASE_CONTEXT_CONTROL" => Ok(Self::CodebaseContextControl),
"SUGGESTED_CODE_DIFFS_CONTROL" => Ok(Self::SuggestedCodeDiffsControl),
"SUGGESTED_CODE_DIFFS_EXPERIMENT" => Ok(Self::SuggestedCodeDiffsExperiment),
"BUILD_PLAN_AUTO_RELOAD_CONTROL" => Ok(Self::BuildPlanAutoReloadControl),
"BUILD_PLAN_AUTO_RELOAD_BANNER_TOGGLE" => Ok(Self::BuildPlanAutoReloadBannerToggle),
"BUILD_PLAN_AUTO_RELOAD_POST_PURCHASE_MODAL" => {
Ok(Self::BuildPlanAutoReloadPostPurchaseModal)
}
"PROMPT_SUGGESTIONS_VIA_MAA_CONTROL" => Ok(Self::PromptSuggestionsViaMaaControl),
"PROMPT_SUGGESTIONS_VIA_MAA_EXPERIMENT" => Ok(Self::PromptSuggestionsViaMaaExperiment),
"FREE_USER_NO_AI_CONTROL" => Ok(Self::FreeUserNoAiControl),
"FREE_USER_NO_AI_EXPERIMENT" => Ok(Self::FreeUserNoAiExperiment),
"OZ_MULTI_HARNESS_CONTROL" => Ok(Self::OzMultiHarnessControl),
"OZ_MULTI_HARNESS_EXPERIMENT" => Ok(Self::OzMultiHarnessExperiment),
s => Err(anyhow::anyhow!(
"String doesn't match any server experiment variant {s}"
)),
}
}
}
impl TryFrom<Experiment> for ServerExperiment {
type Error = anyhow::Error;
fn try_from(value: Experiment) -> Result<Self, Self::Error> {
match value {
Experiment::SessionSharingExperiment => Ok(Self::SessionSharingExperiment),
Experiment::SessionSharingControl => Ok(Self::SessionSharingControl),
Experiment::BuildPlanAutoReloadControl => Ok(Self::BuildPlanAutoReloadControl),
Experiment::BuildPlanAutoReloadBannerToggle => {
Ok(Self::BuildPlanAutoReloadBannerToggle)
}
Experiment::BuildPlanAutoReloadPostPurchaseModal => {
Ok(Self::BuildPlanAutoReloadPostPurchaseModal)
}
Experiment::DisableAgentModeExperiment => Ok(Self::DisableAgentModeExperiment),
Experiment::EnvVarsEarlyAccessExperiment => Ok(Self::EnvVarsEarlyAccessExperiment),
Experiment::AgentModeAnalyticsExperiment => Ok(Self::AgentModeAnalyticsExperiment),
Experiment::TmuxSshWarpificationControl => Ok(Self::TmuxSshWarpificationControl),
Experiment::TmuxSshWarpificationExperiment => Ok(Self::TmuxSshWarpificationExperiment),
Experiment::WindowsLaunchExperiment => Ok(Self::WindowsLaunchExperiment),
Experiment::CodebaseContextControl => Ok(Self::CodebaseContextControl),
Experiment::CodebaseContextExperiment => Ok(Self::CodebaseContextExperiment),
Experiment::SuggestedCodeDiffsControl => Ok(Self::SuggestedCodeDiffsControl),
Experiment::SuggestedCodeDiffsExperiment => Ok(Self::SuggestedCodeDiffsExperiment),
Experiment::PromptSuggestionsViaMaaControl => Ok(Self::PromptSuggestionsViaMaaControl),
Experiment::PromptSuggestionsViaMaaOob => {
Ok(Self::PromptSuggestionsViaMaaOutOfBandExperiment)
}
Experiment::FreeUserNoAiControl => Ok(Self::FreeUserNoAiControl),
Experiment::FreeUserNoAiExperiment => Ok(Self::FreeUserNoAiExperiment),
Experiment::OzMultiHarnessControl => Ok(Self::OzMultiHarnessControl),
Experiment::OzMultiHarnessExperiment => Ok(Self::OzMultiHarnessExperiment),
// Experiments that we no longer support on the client.
e => Err(anyhow::anyhow!(
"Server-side enabled experiment '{e:?}' is no longer supported by the client."
)),
}
}
}
#[macro_export]
macro_rules! convert_to_server_experiment {
($gql_type:expr) => {{
let mut acc = Vec::new();
for a in $gql_type {
// Note for server experiments we don't currently track on the client.
// This could be because the client is out of date and we should still
// apply the experiments the client does track.
if let Ok(b) = ServerExperiment::try_from(a) {
acc.push(b);
}
}
Some(acc)
}};
}
+173
View File
@@ -0,0 +1,173 @@
//! This module is responsible for applying
//! server-side experiment state to the client.
//!
//! After adding support for your experiment on the server,
//! you need to define what effect the experiment will
//! have on the client (i.e. see [`ServerExperiment::on_added_to`]).
//!
//! Then, you can use the global [`ServerExperiments`] model
//! to update and query the latest experiment state.
//!
//! See [here](https://www.notion.so/warpdev/Server-side-experiments-dynamic-feature-enablement-c0fb9aed695d4178a19b8830e3269094)
//! for a full guide on the server-side experiment framework.
use crate::features::FeatureFlag;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::workspaces::workspace::CustomerType;
use warpui::AppContext;
#[cfg(not(test))]
use warpui::SingletonEntity as _;
#[cfg(test)]
use warpui::SingletonEntity;
mod convert;
mod model;
pub use model::{Event as ServerExperimentsEvent, ServerExperiments};
/// The known server-side experiments.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum ServerExperiment {
SessionSharingExperiment,
SessionSharingControl,
DisableAgentModeExperiment,
EnvVarsEarlyAccessExperiment,
AgentModeAnalyticsExperiment,
WindowsLaunchExperiment,
TmuxSshWarpificationControl,
TmuxSshWarpificationExperiment,
CodebaseContextExperiment,
CodebaseContextControl,
SuggestedCodeDiffsControl,
SuggestedCodeDiffsExperiment,
BuildPlanAutoReloadControl,
BuildPlanAutoReloadBannerToggle,
BuildPlanAutoReloadPostPurchaseModal,
PromptSuggestionsViaMaaControl,
PromptSuggestionsViaMaaExperiment,
PromptSuggestionsViaMaaOutOfBandExperiment,
FreeUserNoAiControl,
FreeUserNoAiExperiment,
OzMultiHarnessControl,
OzMultiHarnessExperiment,
/// A test-only experiment.
/// Does not correspond to a real server-side experiment.
#[cfg(test)]
TestExperiment,
}
impl ServerExperiment {
/// When the client is added to an experiment.
///
// TODO: currently, it isn't possible to interact with other
// application singletons in this function because [`ServerExperiments`]
// is initialized before most other singletons during app init. We should either:
// a) remove the `ctx` from this function to prevent reading / updating
// other application entities here, which forces those other entities
// to subscribe to [`ServerExperiments`] updates instead, or
// b) continue initializing [`ServerExperiments`] as one of the first
// singletons but apply the cached state after initializing all other singletons.
// That way, this method would not be called until after all other singletons
// have been initialized and can thus be referenced.
fn on_added_to(&self, _ctx: &mut AppContext) {
match self {
Self::SessionSharingExperiment => {
FeatureFlag::CreatingSharedSessions.set_enabled(true);
}
Self::SessionSharingControl => {
FeatureFlag::CreatingSharedSessions.set_enabled(false);
}
Self::DisableAgentModeExperiment => {
FeatureFlag::AgentMode.set_enabled(false);
}
Self::EnvVarsEarlyAccessExperiment => {
// EnvVars is now always enabled; no-op.
}
Self::AgentModeAnalyticsExperiment => {
FeatureFlag::AgentModeAnalytics.set_enabled(true);
FeatureFlag::AIRules.set_enabled(true);
FeatureFlag::SuggestedRules.set_enabled(true);
}
Self::WindowsLaunchExperiment => {
// TODO(alokedesai): Clean this up now that we no longer gate access to the Windows
// build on an allowlist.
}
Self::TmuxSshWarpificationControl => FeatureFlag::SSHTmuxWrapper.set_enabled(false),
Self::TmuxSshWarpificationExperiment => {
// Only enable the TMUX-based experience if not on windows. ConPTY doesn't support
// DCS, which we need in order to use tmux control mode.
if cfg!(not(windows)) {
FeatureFlag::SSHTmuxWrapper.set_enabled(true)
}
}
Self::CodebaseContextExperiment => {
FeatureFlag::FullSourceCodeEmbedding.set_enabled(true);
FeatureFlag::CodebaseIndexPersistence.set_enabled(true);
FeatureFlag::CodebaseIndexSpeedbump.set_enabled(true);
FeatureFlag::CrossRepoContext.set_enabled(true);
}
Self::CodebaseContextControl => {
FeatureFlag::FullSourceCodeEmbedding.set_enabled(false);
FeatureFlag::CodebaseIndexPersistence.set_enabled(false);
FeatureFlag::CodebaseIndexSpeedbump.set_enabled(false);
FeatureFlag::CrossRepoContext.set_enabled(false);
}
Self::SuggestedCodeDiffsExperiment => {}
Self::SuggestedCodeDiffsControl => {}
Self::BuildPlanAutoReloadControl => {
// Control group - disable both experiment flags
FeatureFlag::BuildPlanAutoReloadBannerToggle.set_enabled(false);
FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.set_enabled(false);
}
Self::BuildPlanAutoReloadBannerToggle => {
// Experiment variant 1 - enable banner toggle modal
FeatureFlag::BuildPlanAutoReloadBannerToggle.set_enabled(true);
FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.set_enabled(false);
}
Self::BuildPlanAutoReloadPostPurchaseModal => {
// Experiment variant 2 - enable post-purchase modal
FeatureFlag::BuildPlanAutoReloadBannerToggle.set_enabled(false);
FeatureFlag::BuildPlanAutoReloadPostPurchaseModal.set_enabled(true);
}
Self::PromptSuggestionsViaMaaControl => {
FeatureFlag::PromptSuggestionsViaMAA.set_enabled(false);
}
Self::PromptSuggestionsViaMaaOutOfBandExperiment => {
FeatureFlag::PromptSuggestionsViaMAA.set_enabled(true);
}
// The normal experiment arm is no longer used.
Self::PromptSuggestionsViaMaaExperiment => {}
Self::FreeUserNoAiControl => {
FeatureFlag::FreeUserNoAi.set_enabled(false);
}
Self::FreeUserNoAiExperiment => {
FeatureFlag::FreeUserNoAi.set_enabled(true);
}
Self::OzMultiHarnessControl => {
FeatureFlag::AgentHarness.set_enabled(false);
}
Self::OzMultiHarnessExperiment => {
FeatureFlag::AgentHarness.set_enabled(true);
}
#[cfg(test)]
Self::TestExperiment => {
model::TestModel::handle(_ctx).update(_ctx, |model, _| {
model.0 += 1;
});
}
}
}
}
/// Returns `true` when the user is in the `FreeUserNoAiExperiment` arm **and** is on the
/// free tier. This is the single source of truth for gating any client-side behaviour
/// that should be locked/disabled for users without AI credits.
pub fn is_free_user_no_ai_experiment_active(ctx: &AppContext) -> bool {
let in_experiment = FeatureFlag::FreeUserNoAi.is_enabled();
let is_free_tier = UserWorkspaces::handle(ctx)
.as_ref(ctx)
.current_team()
.map(|team| team.billing_metadata.customer_type == CustomerType::Free)
.unwrap_or(true); // no team = solo free user
in_experiment && is_free_tier
}
+92
View File
@@ -0,0 +1,92 @@
//! The model for maintaining global experiment state.
use std::collections::HashSet;
use super::ServerExperiment;
use crate::{persistence::ModelEvent, report_if_error, GlobalResourceHandlesProvider};
use anyhow::Context;
use warpui::{Entity, ModelContext, SingletonEntity};
#[cfg(test)]
pub use tests::TestModel;
/// A global model for maintaining server-side experiment state.
pub struct ServerExperiments {
/// The latest-known set of server-side enabled experiments.
latest: HashSet<ServerExperiment>,
}
impl ServerExperiments {
/// Creates a new [`ServerExperiments`] model and seeds it with
/// the provided `cached` experiment state.
pub fn new_from_cache(cached: Vec<ServerExperiment>, ctx: &mut ModelContext<Self>) -> Self {
let mut model = Self {
latest: HashSet::new(),
};
model.apply_latest_state(cached, ctx);
model
}
/// Updates the model with the latest server-side state.
///
/// Assumes the set of proivded [`ServerExperiment`]s are unambiguous;
/// that is, there are not two arms enabled for the same experiment group.
pub fn apply_latest_state(
&mut self,
incoming: Vec<ServerExperiment>,
ctx: &mut ModelContext<Self>,
) {
// Dedup the set of experiments.
let incoming = HashSet::from_iter(incoming);
// For every experiment that the client isn't already part of,
// perform the necessary logic to add them.
for experiment in incoming.difference(&self.latest) {
experiment.on_added_to(ctx);
}
self.cache_latest_state(incoming, ctx);
ctx.emit(Event::ExperimentsUpdated);
}
/// Returns true iff the `experiment` is enabled.
pub fn is_experiment_enabled(&self, experiment: &ServerExperiment) -> bool {
self.latest.contains(experiment)
}
/// Saves the latest experiment state in-memory and to the local cache.
fn cache_latest_state(
&mut self,
latest: HashSet<ServerExperiment>,
ctx: &mut ModelContext<Self>,
) {
self.latest = latest;
if let Some(model_event_sender) = GlobalResourceHandlesProvider::as_ref(ctx)
.get()
.model_event_sender
.as_ref()
{
let event = ModelEvent::SaveExperiments {
experiments: self.latest.iter().copied().collect(),
};
report_if_error!(model_event_sender
.send(event)
.context("Unable to save experiments to sqlite"));
}
}
}
pub enum Event {
ExperimentsUpdated,
}
impl Entity for ServerExperiments {
type Event = Event;
}
impl SingletonEntity for ServerExperiments {}
#[cfg(test)]
#[path = "model_tests.rs"]
mod tests;
+65
View File
@@ -0,0 +1,65 @@
use super::{ServerExperiment, ServerExperiments};
use crate::{GlobalResourceHandles, GlobalResourceHandlesProvider};
use warpui::{App, Entity, SingletonEntity};
/// A model for testing purposes only.
///
/// We use it to demonstrate how client-side
/// models can be mutated to reflect server
/// experiment state changes.
pub struct TestModel(pub usize);
impl Entity for TestModel {
type Event = ();
}
impl SingletonEntity for TestModel {}
fn initialize_app(app: &mut App) {
app.update(crate::settings::init_and_register_user_preferences);
let global_resources = GlobalResourceHandles::mock(app);
app.add_singleton_model(|_| GlobalResourceHandlesProvider::new(global_resources));
}
#[test]
fn test_new_from_cached() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let model = app.add_singleton_model(|_| TestModel(0));
let cache = vec![ServerExperiment::TestExperiment];
app.add_singleton_model(|ctx| ServerExperiments::new_from_cache(cache, ctx));
// The experiment should have been enabled.
model.read(&app, |model, _| {
assert_eq!(model.0, 1);
});
});
}
#[test]
fn test_apply_latest_state() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let model = app.add_singleton_model(|_| TestModel(0));
let experiments =
app.add_singleton_model(|ctx| ServerExperiments::new_from_cache(vec![], ctx));
// Enable the experiment.
experiments.update(&mut app, |experiments, ctx| {
experiments.apply_latest_state(vec![ServerExperiment::TestExperiment], ctx);
});
model.read(&app, |model, _| {
assert_eq!(model.0, 1);
});
// Redundant experiment state should be a no-op.
experiments.update(&mut app, |experiments, ctx| {
experiments.apply_latest_state(vec![ServerExperiment::TestExperiment], ctx);
});
model.read(&app, |model, _| {
assert_eq!(model.0, 1);
});
});
}