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,130 @@
use super::search_item::WorkflowSearchItem;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::CloudModelType;
use crate::search::ai_context_menu::mixer::AIContextMenuSearchableAction;
use crate::search::data_source::{Query, QueryResult};
use crate::search::mixer::{DataSourceRunErrorWrapper, SyncDataSource};
use crate::workspaces::user_workspaces::UserWorkspaces;
use fuzzy_match::FuzzyMatchResult;
use warpui::{AppContext, SingletonEntity};
const MAX_RESULTS: usize = 50;
/// Base score for zero-state results. Each item gets an additional bonus based on
/// recency so the mixer's score-based ordering places more recent items higher.
const ZERO_STATE_BASE_SCORE: i64 = 1000;
pub struct WorkflowDataSource;
impl WorkflowDataSource {
#[allow(dead_code)]
pub fn new() -> Self {
Self
}
}
impl SyncDataSource for WorkflowDataSource {
type Action = AIContextMenuSearchableAction;
fn run_query(
&self,
query: &Query,
app: &AppContext,
) -> Result<Vec<QueryResult<Self::Action>>, DataSourceRunErrorWrapper> {
let query_text = &query.text;
// Get all workflows from CloudModel
let cloud_model = CloudModel::as_ref(app);
let _user_workspaces = UserWorkspaces::as_ref(app);
// Get workflows from all spaces the user has access to
let mut workflow_results = Vec::new();
// Collect non-welcome workflows, sorted by revision timestamp in zero state
let mut workflows: Vec<_> = cloud_model
.get_all_active_workflows()
.filter(|w| !w.metadata.is_welcome_object)
.collect();
// Always sort by revision timestamp ascending so that position-based
// scores assign higher values to more recently updated items. This ensures
// recency acts as a tiebreaker when fuzzy scores are similar.
workflows.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_workflows = workflows.len();
for (index, workflow) in workflows.into_iter().enumerate() {
let workflow_name = workflow.model().display_name();
// Use workflow content for hover details, with first few lines as preview
let workflow_content = workflow.model().data.content();
let content_lines: Vec<&str> = workflow_content.lines().take(3).collect();
let content_preview = content_lines.join("\n");
let workflow_description = if content_preview.is_empty() {
None
} else {
Some(if content_preview.len() > 200 {
format!("{}...", &content_preview[..197])
} else {
content_preview
})
};
let workflow_uid = workflow.id.uid();
let recency_bonus = (30 * (index + 1) / total_workflows) as i64;
let (match_result, is_match_on_name) = if query_text.is_empty() {
// Zero state: score encodes recency so the mixer orders newest items highest.
(
FuzzyMatchResult {
score: ZERO_STATE_BASE_SCORE + recency_bonus,
matched_indices: vec![],
},
false,
)
} else {
// Fuzzy match against workflow name
let name_match =
fuzzy_match::match_indices_case_insensitive(&workflow_name, query_text);
// Also try matching against description if available
let description_match = workflow_description
.as_deref()
.and_then(|desc| fuzzy_match::match_indices_case_insensitive(desc, query_text));
// Use the best match, tracking whether it was on the name
let (mut result, on_name) = match (name_match, description_match) {
(Some(name), Some(desc)) if desc.score > name.score => (desc, false),
(Some(name), _) => (name, true),
(None, Some(desc)) => (desc, false),
(None, None) => continue, // No match, skip this workflow
};
// Add a recency bonus (capped at 30) so more recently updated
// items rank higher among results with similar fuzzy scores,
// regardless of the total size of the workflows collection.
result.score += recency_bonus;
(result, on_name)
};
let search_item = WorkflowSearchItem {
workflow_name,
workflow_description,
workflow_uid,
match_result,
is_match_on_name,
};
workflow_results.push(QueryResult::from(search_item));
}
// Sort by score and take the top results
workflow_results.sort_by_key(|b| std::cmp::Reverse(b.score()));
workflow_results.truncate(MAX_RESULTS);
Ok(workflow_results)
}
}
impl warpui::Entity for WorkflowDataSource {
type Event = ();
}
@@ -0,0 +1,2 @@
pub mod data_source;
pub mod search_item;
@@ -0,0 +1,219 @@
use fuzzy_match::FuzzyMatchResult;
use ordered_float::OrderedFloat;
use std::fmt::Debug;
use crate::appearance::Appearance;
use crate::cloud_object::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 WorkflowSearchItem {
pub workflow_name: String,
pub workflow_description: Option<String>,
pub workflow_uid: String,
pub match_result: FuzzyMatchResult,
/// True if match_result was computed against the workflow name (vs description)
pub is_match_on_name: bool,
}
impl SearchItem for WorkflowSearchItem {
type Action = AIContextMenuSearchableAction;
fn render_icon(
&self,
highlight_state: ItemHighlightState,
appearance: &Appearance,
) -> Box<dyn Element> {
Container::new(
ConstrainedBox::new(
Icon::new(
"bundled/svg/workflow.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);
let mut workflow_name = self.workflow_name.clone();
let mut workflow_description = self
.workflow_description
.as_deref()
.unwrap_or("")
.to_string();
// Track if we truncated anything for highlight adjustment
let mut name_truncated = false;
// Ensure combined length is reasonable
let combined_length = workflow_name.len() + workflow_description.len();
if combined_length > MAX_COMBINED_LENGTH {
// Prioritize showing the workflow name
if workflow_name.len() >= MAX_COMBINED_LENGTH {
safe_truncate(&mut workflow_name, MAX_COMBINED_LENGTH - 3);
workflow_name.push_str("...");
name_truncated = true;
workflow_description.clear();
} else {
// Workflow name fits, truncate description
let available_for_description = MAX_COMBINED_LENGTH - workflow_name.len();
if workflow_description.len() > available_for_description {
safe_truncate(
&mut workflow_description,
available_for_description.saturating_sub(3),
);
workflow_description.push_str("...");
}
}
}
// Calculate highlight indices based on where match occurred
let name_highlights = if !self.match_result.matched_indices.is_empty()
&& !name_truncated
&& self.is_match_on_name
{
self.match_result.matched_indices.clone()
} else {
vec![]
};
let description_highlights = if !self.match_result.matched_indices.is_empty()
&& !self.is_match_on_name
&& !workflow_description.is_empty()
{
self.match_result.matched_indices.clone()
} else {
vec![]
};
// Create workflow name with match highlighting
let mut name_text = Text::new(
workflow_name,
appearance.ui_font_family(),
appearance.monospace_font_size() - 1.0,
)
.with_color(highlight_state.main_text_fill(appearance).into_solid());
if !name_highlights.is_empty() {
name_text = name_text.with_single_highlight(
Highlight::new().with_properties(Properties::default().weight(Weight::Bold)),
name_highlights,
);
}
// Create description text with lighter color
let description_text = if !workflow_description.is_empty() {
let mut desc_text = Text::new(
workflow_description,
appearance.ui_font_family(),
appearance.monospace_font_size() - 2.0,
)
.with_color(highlight_state.sub_text_fill(appearance).into_solid());
if !description_highlights.is_empty() {
desc_text = desc_text.with_single_highlight(
Highlight::new().with_properties(Properties::default().weight(Weight::Bold)),
description_highlights,
);
}
Some(desc_text)
} else {
None
};
// Create row with workflow name and description
let mut row = Flex::row()
.with_child(name_text.finish())
.with_cross_axis_alignment(CrossAxisAlignment::Center);
if let Some(description) = description_text {
row.add_child(
Container::new(description.finish())
.with_padding_left(6.)
.finish(),
);
}
row.finish()
}
fn score(&self) -> OrderedFloat<f64> {
OrderedFloat(self.match_result.score as f64)
}
fn accept_result(&self) -> Self::Action {
AIContextMenuSearchableAction::InsertDriveObject {
object_type: ObjectType::Workflow,
object_uid: self.workflow_uid.clone(),
}
}
fn execute_result(&self) -> Self::Action {
self.accept_result()
}
fn accessibility_label(&self) -> String {
if let Some(description) = &self.workflow_description {
format!("Workflow: {} - {}", self.workflow_name, description)
} else {
format!("Workflow: {}", self.workflow_name)
}
}
fn render_details(&self, ctx: &AppContext) -> Option<Box<dyn Element>> {
let appearance = Appearance::as_ref(ctx);
let name_element = Text::new(
self.workflow_name.clone(),
appearance.ui_font_family(),
appearance.monospace_font_size() - 1.0,
)
.with_color(appearance.theme().active_ui_text_color().into());
let details = if let Some(description) = &self.workflow_description {
let content_element = Text::new(
description.clone(),
appearance.monospace_font_family(),
appearance.monospace_font_size() - 2.0,
)
.with_color(appearance.theme().nonactive_ui_text_color().into());
Flex::column()
.with_child(name_element.finish())
.with_child(
Container::new(content_element.finish())
.with_padding_top(4.0)
.finish(),
)
.finish()
} else {
Flex::column().with_child(name_element.finish()).finish()
};
Some(details)
}
}