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
@@ -0,0 +1,109 @@
use super::search_item::RuleSearchItem;
use crate::ai::facts::{AIFact, CloudAIFactModel};
use crate::cloud_object::model::generic_string_model::GenericStringObjectId;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::CloudObject;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, Entity, SingletonEntity};
const MAX_RESULTS: usize = 50;
const ZERO_STATE_BASE_SCORE: i64 = 1000;
pub struct RulesDataSource;
impl RulesDataSource {
#[allow(dead_code)]
pub fn new() -> Self {
Self
}
}
impl SyncDataSource for RulesDataSource {
type Action = AIContextMenuSearchableAction;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper> {
let query_text = &query.text;
let cloud_model = CloudModel::as_ref(app);
let mut rule_results = Vec::new();
let mut rules: Vec<_> = cloud_model
.get_all_objects_of_type::<GenericStringObjectId, CloudAIFactModel>()
.filter(|ai_fact| !ai_fact.is_trashed(cloud_model))
.collect();
// Sort by revision timestamp ascending so that position-based scores
// assign higher values to more recently updated rules.
rules.sort_by(|a, b| {
let a_ts = a.metadata.revision.as_ref().map(|r| r.timestamp());
let b_ts = b.metadata.revision.as_ref().map(|r| r.timestamp());
a_ts.cmp(&b_ts)
});
let total_rules = rules.len();
for (index, ai_fact) in rules.into_iter().enumerate() {
let rule_uid = ai_fact.id.uid();
let (rule_name, rule_content) = match &ai_fact.model().string_model {
AIFact::Memory(memory) => (memory.name.clone(), memory.content.clone()),
};
let (match_result, is_match_on_rule_name) = if query_text.is_empty() {
(
FuzzyMatchResult {
score: ZERO_STATE_BASE_SCORE + index as i64,
matched_indices: vec![],
},
false,
)
} else {
let name_match = rule_name
.as_ref()
.and_then(|n| fuzzy_match::match_indices_case_insensitive(n, query_text));
let content_match =
fuzzy_match::match_indices_case_insensitive(&rule_content, query_text);
let (mut result, on_name) = match (name_match, content_match) {
(Some(name), Some(content)) if content.score > name.score => (content, false),
(Some(name), _) => (name, true),
(None, Some(content)) => (content, false),
(None, None) => continue,
};
// Add a recency bonus (capped at 30) so more recently updated
// rules rank higher among results with similar fuzzy scores,
// regardless of the total size of the rules collection.
result.score += (30 * (index + 1) / total_rules) as i64;
(result, on_name)
};
let search_item = RuleSearchItem {
rule_uid,
rule_name,
rule_content,
match_result,
is_match_on_rule_name,
};
rule_results.push(QueryResult::from(search_item));
}
// Sort by score and take the top results
rule_results.sort_by_key(|b| std::cmp::Reverse(b.score()));
rule_results.truncate(MAX_RESULTS);
Ok(rule_results)
}
}
impl Entity for RulesDataSource {
type Event = ();
}
#[cfg(test)]
#[path = "data_source_tests.rs"]
mod tests;
@@ -0,0 +1,192 @@
use std::sync::Arc;
use chrono::{Duration, Utc};
use settings::manager::SettingsManager;
use warpui::{App, SingletonEntity};
use crate::ai::facts::{AIFact, AIMemory, CloudAIFactModel};
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::generic_string_model::GenericStringModel;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::model::view::CloudViewModel;
use crate::cloud_object::{
GenericServerObject, Owner, Revision, ServerMetadata, ServerPermissions,
};
use crate::notebooks::manager::NotebookManager;
use crate::search::ai_context_menu::rules::data_source::RulesDataSource;
use crate::search::data_source::Query;
use crate::search::mixer::SyncDataSource;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::{ServerId, SyncId};
use crate::server::server_api::ServerApiProvider;
use crate::server::sync_queue::SyncQueue;
use crate::settings::AISettings;
use crate::system::SystemStats;
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::user_profiles::UserProfiles;
use crate::workspaces::user_workspaces::UserWorkspaces;
use crate::NetworkStatus;
use crate::server::server_api::object::MockObjectClient;
use crate::server::server_api::team::MockTeamClient;
use crate::server::server_api::workspace::MockWorkspaceClient;
type ServerAIFact = GenericServerObject<
crate::cloud_object::model::generic_string_model::GenericStringObjectId,
CloudAIFactModel,
>;
fn mock_server_ai_fact(id: i64, name: &str, content: &str, revision: Revision) -> ServerAIFact {
GenericServerObject {
id: SyncId::ServerId(id.into()),
metadata: ServerMetadata {
uid: ServerId::default(),
revision,
metadata_last_updated_ts: Utc::now().into(),
trashed_ts: None,
folder_id: None,
is_welcome_object: false,
creator_uid: None,
last_editor_uid: None,
current_editor_uid: None,
},
permissions: ServerPermissions {
space: Owner::mock_current_user(),
guests: Vec::new(),
anyone_link_sharing: None,
permissions_last_updated_ts: Utc::now().into(),
},
model: GenericStringModel {
string_model: AIFact::Memory(AIMemory {
name: Some(name.to_string()),
content: content.to_string(),
is_autogenerated: false,
suggested_logging_id: None,
}),
},
}
}
fn initialize_app(app: &mut App) {
app.add_singleton_model(|_| NetworkStatus::new());
app.add_singleton_model(|_| SystemStats::new());
let mock_team_client = Arc::new(MockTeamClient::new());
let mock_workspace_client = Arc::new(MockWorkspaceClient::new());
app.add_singleton_model(|ctx| {
UserWorkspaces::mock(
mock_team_client.clone(),
mock_workspace_client.clone(),
vec![],
ctx,
)
});
app.add_singleton_model(TeamTesterStatus::new);
app.add_singleton_model(SyncQueue::mock);
app.add_singleton_model(CloudModel::mock);
app.add_singleton_model(|ctx| UpdateManager::new(None, Arc::new(MockObjectClient::new()), ctx));
app.add_singleton_model(|_| UserProfiles::new(Vec::new()));
app.add_singleton_model(CloudViewModel::new);
app.add_singleton_model(NotebookManager::mock);
app.add_singleton_model(|_| ServerApiProvider::new_for_test());
app.add_singleton_model(|_| SettingsManager::default());
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
app.update(crate::settings::init_and_register_user_preferences);
app.update(AISettings::register_and_subscribe_to_events);
}
#[test]
fn zero_state_scores_reflect_recency() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let now = Utc::now();
CloudModel::handle(&app).update(&mut app, |model, ctx| {
model.upsert_from_server_object(
mock_server_ai_fact(
1,
"oldest rule",
"oldest content",
(now - Duration::minutes(3)).into(),
),
ctx,
);
model.upsert_from_server_object(
mock_server_ai_fact(
2,
"middle rule",
"middle content",
(now - Duration::minutes(2)).into(),
),
ctx,
);
model.upsert_from_server_object(
mock_server_ai_fact(
3,
"newest rule",
"newest content",
(now - Duration::minutes(1)).into(),
),
ctx,
);
});
let data_source = RulesDataSource::new();
let results = app.read(|app| data_source.run_query(&Query::from(""), app).unwrap());
assert_eq!(results.len(), 3);
let scores: Vec<_> = results.iter().map(|r| r.score()).collect();
assert!(
scores[0] > scores[1] && scores[1] > scores[2],
"Expected scores in strictly descending order (newest first), got {scores:?}"
);
})
}
#[test]
fn filtered_state_adds_recency_bonus() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let now = Utc::now();
// All rules contain "rule" so fuzzy scores should be similar
CloudModel::handle(&app).update(&mut app, |model, ctx| {
model.upsert_from_server_object(
mock_server_ai_fact(
1,
"my first rule",
"first rule content",
(now - Duration::minutes(3)).into(),
),
ctx,
);
model.upsert_from_server_object(
mock_server_ai_fact(
2,
"my second rule",
"second rule content",
(now - Duration::minutes(2)).into(),
),
ctx,
);
model.upsert_from_server_object(
mock_server_ai_fact(
3,
"my third rule",
"third rule content",
(now - Duration::minutes(1)).into(),
),
ctx,
);
});
let data_source = RulesDataSource::new();
let results = app.read(|app| data_source.run_query(&Query::from("rule"), app).unwrap());
assert_eq!(results.len(), 3);
let scores: Vec<_> = results.iter().map(|r| r.score()).collect();
assert!(
scores[0] > scores[1] && scores[1] > scores[2],
"Expected scores in strictly descending order (newest first), got {scores:?}"
);
})
}
@@ -0,0 +1,2 @@
pub mod data_source;
pub mod search_item;
@@ -0,0 +1,230 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use crate::appearance::Appearance;
use crate::cloud_object::{GenericStringObjectFormat, JsonObjectType, ObjectType};
use crate::search::ai_context_menu::styles;
use crate::search::ai_context_menu::{mixer::AIContextMenuSearchableAction, safe_truncate};
use crate::search::item::SearchItem;
use crate::search::result_renderer::ItemHighlightState;
use warpui::elements::{
ConstrainedBox, Container, CrossAxisAlignment, Flex, Highlight, Icon, ParentElement, Text,
};
use warpui::fonts::{Properties, Weight};
use warpui::{AppContext, Element, SingletonEntity};
const MAX_COMBINED_LENGTH: usize = 55;
#[derive(Debug)]
pub struct RuleSearchItem {
pub rule_uid: String,
pub rule_name: Option<String>,
pub rule_content: String,
pub match_result: FuzzyMatchResult,
/// True if match_result was computed against the rule name (vs content)
pub is_match_on_rule_name: bool,
}
impl SearchItem for RuleSearchItem {
type Action = AIContextMenuSearchableAction;
fn render_icon(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Box<dyn Element> {
Container::new(
ConstrainedBox::new(
Icon::new(
"bundled/svg/book-open.svg",
highlight_state.icon_fill(appearance).into_solid(),
)
.finish(),
)
.with_width(styles::ICON_SIZE)
.with_height(styles::ICON_SIZE)
.finish(),
)
.with_margin_right(styles::MARGIN_RIGHT)
.finish()
}
fn render_item(
&self,
highlight_state: ItemHighlightState,
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
// Use rule_name if available, otherwise fall back to rule_content
let (primary_text, secondary_text, is_match_on_primary) = match &self.rule_name {
Some(name) if !name.is_empty() => (
name.clone(),
Some(self.rule_content.clone()),
self.is_match_on_rule_name,
),
_ => (self.rule_content.clone(), None, true),
};
let mut display_primary = primary_text;
let mut display_secondary = secondary_text.unwrap_or_default();
let mut primary_truncated = false;
// Ensure combined length is reasonable
let combined_length = display_primary.len() + display_secondary.len();
if combined_length > MAX_COMBINED_LENGTH {
if display_primary.len() >= MAX_COMBINED_LENGTH {
safe_truncate(&mut display_primary, MAX_COMBINED_LENGTH - 3);
display_primary.push_str("...");
primary_truncated = true;
display_secondary.clear();
} else {
let available_for_secondary = MAX_COMBINED_LENGTH - display_primary.len();
if display_secondary.len() > available_for_secondary {
safe_truncate(
&mut display_secondary,
available_for_secondary.saturating_sub(3),
);
display_secondary.push_str("...");
}
}
}
// Calculate highlight indices for primary or secondary text based on where match occurred
let primary_highlights = if !self.match_result.matched_indices.is_empty()
&& !primary_truncated
&& is_match_on_primary
{
self.match_result.matched_indices.clone()
} else {
vec![]
};
let secondary_highlights = if !self.match_result.matched_indices.is_empty()
&& !is_match_on_primary
&& !display_secondary.is_empty()
{
self.match_result.matched_indices.clone()
} else {
vec![]
};
let mut primary_text_element = Text::new(
display_primary,
appearance.ui_font_family(),
appearance.monospace_font_size() - 1.0,
)
.with_color(highlight_state.main_text_fill(appearance).into_solid());
if !primary_highlights.is_empty() {
primary_text_element = primary_text_element.with_single_highlight(
Highlight::new().with_properties(Properties::default().weight(Weight::Bold)),
primary_highlights,
);
}
let secondary_text_element = if !display_secondary.is_empty() {
let mut secondary_text = Text::new(
display_secondary,
appearance.ui_font_family(),
appearance.monospace_font_size() - 2.0,
)
.with_color(highlight_state.sub_text_fill(appearance).into_solid());
if !secondary_highlights.is_empty() {
secondary_text = secondary_text.with_single_highlight(
Highlight::new().with_properties(Properties::default().weight(Weight::Bold)),
secondary_highlights,
);
}
Some(secondary_text)
} else {
None
};
let mut row = Flex::row()
.with_child(primary_text_element.finish())
.with_cross_axis_alignment(CrossAxisAlignment::Center);
if let Some(secondary) = secondary_text_element {
row.add_child(
Container::new(secondary.finish())
.with_padding_left(6.)
.finish(),
);
}
row.finish()
}
fn render_details(&self, ctx: &AppContext) -> Option<Box<dyn Element>> {
let appearance = Appearance::as_ref(ctx);
let theme = appearance.theme();
// Determine what to show as the main title
let title = if let Some(name) = &self.rule_name {
if !name.is_empty() {
name.clone()
} else {
"Rule".to_string()
}
} else {
"Rule".to_string()
};
// Create title element
let title_element = Text::new(
title,
appearance.ui_font_family(),
appearance.monospace_font_size(),
)
.with_color(theme.active_ui_text_color().into())
.with_style(Properties::default().weight(Weight::Bold))
.finish();
// Create content element - show the full rule content
let content_element = Text::new(
self.rule_content.clone(),
appearance.monospace_font_family(),
appearance.monospace_font_size() - 1.0,
)
.with_color(theme.nonactive_ui_text_color().into())
.finish();
// Create the details content
let content = Flex::column()
.with_child(title_element)
.with_child(
Container::new(content_element)
.with_padding_top(8.0)
.finish(),
)
.finish();
Some(content)
}
fn score(&self) -> OrderedFloat<f64> {
OrderedFloat(self.match_result.score as f64)
}
fn accept_result(&self) -> Self::Action {
AIContextMenuSearchableAction::InsertDriveObject {
object_type: ObjectType::GenericStringObject(GenericStringObjectFormat::Json(
JsonObjectType::AIFact,
)),
object_uid: self.rule_uid.clone(),
}
}
fn execute_result(&self) -> Self::Action {
self.accept_result()
}
fn accessibility_label(&self) -> String {
format!("Rule: {}", self.rule_content)
}
}