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,80 @@
use crate::coding_entrypoints::glowing_editor::{GlowingEditor, GlowingEditorEvent};
use crate::TelemetryEvent;
use warp_core::send_telemetry_from_ctx;
use warpui::{
elements::{ChildView, Flex, ParentElement as _},
AppContext, Element, Entity, FocusContext, TypedActionView, View, ViewContext, ViewHandle,
};
pub struct CloneRepoView {
editor: ViewHandle<GlowingEditor>,
is_ftux: bool,
}
pub enum CloneRepoEvent {
SubmitPrompt(String),
Cancel,
}
impl CloneRepoView {
pub fn new(is_ftux: bool, ctx: &mut ViewContext<Self>) -> Self {
let editor = ctx.add_typed_action_view(|ctx| {
GlowingEditor::new(
"Provide a repository URL e.g. \"git@github.com:username/project.git\"",
ctx,
)
});
ctx.subscribe_to_view(&editor, move |me, _, event, ctx| {
me.handle_editor_event(event, ctx);
});
Self { editor, is_ftux }
}
fn handle_editor_event(&mut self, event: &GlowingEditorEvent, ctx: &mut ViewContext<Self>) {
match event {
GlowingEditorEvent::Submit(prompt) => {
send_telemetry_from_ctx!(
TelemetryEvent::CloneRepoPromptSubmitted {
is_ftux: self.is_ftux
},
ctx
);
ctx.emit(CloneRepoEvent::SubmitPrompt(prompt.clone()))
}
GlowingEditorEvent::Cancel => {
self.editor.update(ctx, |editor, ctx| {
editor.clear_buffer(ctx);
});
ctx.emit(CloneRepoEvent::Cancel)
}
}
}
}
impl Entity for CloneRepoView {
type Event = CloneRepoEvent;
}
impl TypedActionView for CloneRepoView {
type Action = ();
}
impl View for CloneRepoView {
fn ui_name() -> &'static str {
"CloneRepoView"
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
ctx.focus(&self.editor);
}
}
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
Flex::column()
.with_child(ChildView::new(&self.editor).finish())
.finish()
}
}
@@ -0,0 +1,240 @@
use crate::ai::blocklist::telemetry_banner::should_collect_ai_ugc_telemetry;
use crate::appearance::Appearance;
use crate::coding_entrypoints::glowing_editor::{GlowingEditor, GlowingEditorEvent};
use crate::settings::PrivacySettings;
use crate::TelemetryEvent;
use warp_core::{send_telemetry_from_ctx, ui::icons::Icon};
use warpui::elements::{ChildView, Expanded, Fill, MainAxisAlignment, MainAxisSize};
use warpui::{
elements::{
ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable,
MouseStateHandle, ParentElement as _, Radius, Text,
},
fonts::{Properties, Weight},
platform::Cursor,
AppContext, Element, Entity, FocusContext, SingletonEntity as _, TypedActionView, View,
ViewContext, ViewHandle,
};
const ICON_MARGIN_LEFT: f32 = 12.;
const ICON_MARGIN_RIGHT: f32 = 8.;
const SUGGESTION_ITEM_PADDING: f32 = 12.;
pub struct CreateProjectView {
editor: ViewHandle<GlowingEditor>,
suggestions: Vec<BuildSuggestion>,
is_ftux: bool,
}
struct BuildSuggestion {
prompt: &'static str,
mouse_state: MouseStateHandle,
}
impl CreateProjectView {
pub fn new(is_ftux: bool, ctx: &mut ViewContext<Self>) -> Self {
let editor =
ctx.add_typed_action_view(|ctx| GlowingEditor::new("What do you want to build?", ctx));
ctx.subscribe_to_view(&editor, move |me, _, event, ctx| {
me.handle_editor_event(event, ctx);
});
let suggestions = vec![
BuildSuggestion {
prompt: "Build a Minesweeper clone in React",
mouse_state: Default::default(),
},
BuildSuggestion {
prompt: "Code a Node.js server that returns random quotes from a JSON file",
mouse_state: Default::default(),
},
BuildSuggestion {
prompt: "Write a CSV to JSON converter CLI",
mouse_state: Default::default(),
},
BuildSuggestion {
prompt: "Create a starter template for a résumé web page",
mouse_state: Default::default(),
},
BuildSuggestion {
prompt: "Make a Conway's Game of Life simulation",
mouse_state: Default::default(),
},
];
Self {
editor,
suggestions,
is_ftux,
}
}
fn handle_editor_event(&mut self, event: &GlowingEditorEvent, ctx: &mut ViewContext<Self>) {
match event {
GlowingEditorEvent::Submit(prompt) => {
// Always send metadata event for custom prompts
send_telemetry_from_ctx!(
TelemetryEvent::CreateProjectPromptSubmitted {
is_custom_prompt: true,
suggested_prompt: None,
is_ftux: self.is_ftux,
},
ctx
);
// Send content event only if UGC collection is enabled
let should_collect_ugc = should_collect_ai_ugc_telemetry(
ctx,
PrivacySettings::as_ref(ctx).is_telemetry_enabled,
);
if should_collect_ugc {
send_telemetry_from_ctx!(
TelemetryEvent::CreateProjectPromptSubmittedContent {
custom_prompt: prompt.clone(),
},
ctx
);
}
ctx.emit(CreateProjectEvent::SubmitPrompt(prompt.clone()));
}
GlowingEditorEvent::Cancel => {
self.editor.update(ctx, |editor, ctx| {
editor.clear_buffer(ctx);
});
ctx.emit(CreateProjectEvent::Cancel)
}
}
}
fn render_suggestion_item(
&self,
appearance: &Appearance,
suggestion: &BuildSuggestion,
) -> Box<dyn Element> {
let theme = appearance.theme();
let icon = Icon::MessagePlusSquare;
let icon_color = theme.terminal_colors().normal.cyan.into();
let font_family = appearance.ui_font_family();
let font_size = appearance.ui_font_size();
let font_color = theme.sub_text_color(theme.background()).into_solid();
let mouse_state = suggestion.mouse_state.clone();
let prompt = suggestion.prompt;
let row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_alignment(MainAxisAlignment::Start)
.with_main_axis_size(MainAxisSize::Max)
.with_children([
Container::new(
ConstrainedBox::new(icon.to_warpui_icon(icon_color).finish())
.with_height(font_size)
.with_width(font_size)
.finish(),
)
.with_margin_left(ICON_MARGIN_LEFT + 2.)
.with_margin_right(ICON_MARGIN_RIGHT)
.finish(),
Expanded::new(
1.,
Text::new(prompt, font_family, font_size)
.with_color(font_color)
.with_style(Properties::default().weight(Weight::Medium))
.soft_wrap(false)
.finish(),
)
.finish(),
]);
Hoverable::new(mouse_state, move |state| {
Container::new(row.finish())
.with_vertical_padding(SUGGESTION_ITEM_PADDING)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_background(if state.is_hovered() {
theme.surface_overlay_1().into()
} else {
Fill::None
})
.finish()
})
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, _| {
ctx.dispatch_typed_action(CreateProjectAction::SuggestionSelected {
prompt: prompt.to_string(),
});
})
.finish()
}
}
pub enum CreateProjectEvent {
SubmitPrompt(String),
Cancel,
}
impl Entity for CreateProjectView {
type Event = CreateProjectEvent;
}
#[derive(Clone, Debug)]
pub enum CreateProjectAction {
SuggestionSelected { prompt: String },
}
impl TypedActionView for CreateProjectView {
type Action = CreateProjectAction;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
CreateProjectAction::SuggestionSelected { prompt } => {
// Always send metadata event with suggested prompt content (non-UGC)
send_telemetry_from_ctx!(
TelemetryEvent::CreateProjectPromptSubmitted {
is_custom_prompt: false,
suggested_prompt: Some(prompt.clone()),
is_ftux: self.is_ftux,
},
ctx
);
ctx.emit(CreateProjectEvent::SubmitPrompt(prompt.clone()));
}
}
}
}
impl View for CreateProjectView {
fn ui_name() -> &'static str {
"CreateProjectView"
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
ctx.focus(&self.editor);
}
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let mut column = Flex::column().with_child(ChildView::new(&self.editor).finish());
if !self.suggestions.is_empty() {
let suggestions = self
.suggestions
.iter()
.map(|suggestion| self.render_suggestion_item(appearance, suggestion))
.collect::<Vec<_>>();
let suggestion_container =
Container::new(Flex::column().with_children(suggestions).finish())
.with_margin_top(8.)
.finish();
column.add_child(suggestion_container);
}
column.finish()
}
}
@@ -0,0 +1,202 @@
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::Vector2F;
use warp_core::ui::{appearance::Appearance, Icon};
use warpui::{
elements::{
Align, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex,
MainAxisAlignment, ParentElement as _, Radius, Shrinkable,
},
fonts::Weight,
ui_components::components::{BorderStyle, Coords, UiComponent as _, UiComponentStyles},
AppContext, Element, Entity, FocusContext, SingletonEntity as _, TypedActionView, View,
ViewContext, ViewHandle,
};
use crate::editor::{EditorOptions, EditorView, Event as EditorEvent, TextOptions};
const PROMPT_INPUT_HEIGHT: f32 = 56.;
const ICON_MARGIN_LEFT: f32 = 12.;
const ICON_MARGIN_RIGHT: f32 = 6.;
pub struct GlowingEditor {
editor: ViewHandle<EditorView>,
/// A closure that returns if the current editor content are valid and can be submitted.
validator: Box<dyn Fn(&str) -> bool>,
/// Whether or not the last submission attempt failed validation.
has_error: bool,
}
impl GlowingEditor {
pub fn new(placeholder: impl Into<String>, ctx: &mut ViewContext<Self>) -> Self {
let appearance = Appearance::as_ref(ctx);
let font_family = appearance.ui_font_family();
let font_size = appearance.ui_font_size() + 2.;
let editor = ctx.add_typed_action_view(|ctx| {
let options = EditorOptions {
soft_wrap: true,
text: TextOptions {
font_size_override: Some(font_size),
font_family_override: Some(font_family),
..Default::default()
},
..Default::default()
};
let mut editor = EditorView::new(options, ctx);
editor.set_placeholder_text(placeholder, ctx);
editor
});
ctx.subscribe_to_view(&editor, move |me, _, event, ctx| {
me.handle_editor_event(event, ctx);
});
Self {
editor,
validator: Box::new(|_| true),
has_error: false,
}
}
/// Validates the input contents using the provided `validator` whenever a submit action is
/// attempted.
#[expect(dead_code, reason = "Nothing needs validation currently.")]
pub fn with_validator<F: Fn(&str) -> bool + 'static>(mut self, validator: F) -> Self {
self.validator = Box::new(validator);
self
}
pub fn clear_buffer(&mut self, ctx: &mut ViewContext<Self>) {
self.editor.update(ctx, |editor, ctx| {
editor.clear_buffer(ctx);
});
}
fn handle_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext<Self>) {
match event {
EditorEvent::Enter => {
let prompt = self
.editor
.read(ctx, |editor, ctx| editor.buffer_text(ctx).trim().to_owned());
if prompt.is_empty() {
return;
}
if !(self.validator)(&prompt) {
self.has_error = true;
ctx.notify();
} else {
self.has_error = false;
self.clear_buffer(ctx);
ctx.emit(GlowingEditorEvent::Submit(prompt));
}
}
EditorEvent::Escape => ctx.emit(GlowingEditorEvent::Cancel),
// Clear error state when user types (since this is submit-only validation)
EditorEvent::Edited(_) => {
if self.has_error {
self.has_error = false;
ctx.notify();
}
}
_ => (),
}
}
}
pub enum GlowingEditorEvent {
Submit(String),
Cancel,
}
impl Entity for GlowingEditor {
type Event = GlowingEditorEvent;
}
impl TypedActionView for GlowingEditor {
type Action = ();
}
impl View for GlowingEditor {
fn ui_name() -> &'static str {
"GlowingEditor"
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
ctx.focus(&self.editor);
}
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let input_box = Shrinkable::new(
1.,
Align::new(
appearance
.ui_builder()
.text_input(self.editor.clone())
.with_style(UiComponentStyles {
font_weight: Some(Weight::Semibold),
border_style: Some(BorderStyle::None),
border_width: Some(0.),
background: Some(ColorU::transparent_black().into()),
padding: Some(Coords::uniform(10.).left(0.)),
..Default::default()
})
.build()
.finish(),
)
.finish(),
)
.finish();
let font_size = appearance.ui_font_size() + 2.;
let agent_icon = Container::new(
ConstrainedBox::new(
Icon::AgentMode
.to_warpui_icon(theme.sub_text_color(theme.background()))
.finish(),
)
.with_height(font_size)
.with_width(font_size)
.finish(),
)
.with_margin_left(ICON_MARGIN_LEFT)
.with_margin_right(ICON_MARGIN_RIGHT)
.finish();
let editor_content = ConstrainedBox::new(
Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_alignment(MainAxisAlignment::Start)
.with_children([agent_icon, input_box])
.finish(),
)
.with_min_height(PROMPT_INPUT_HEIGHT);
let border_fill = if self.has_error {
theme.ui_error_color()
} else {
theme.outline().into_solid()
};
let shadow_color = if self.has_error {
ColorU::new(255, 0, 0, 100) // Red shadow with higher opacity for error
} else {
ColorU::new(255, 143, 253, 15) // Default purple shadow
};
Container::new(editor_content.finish())
.with_border(border_fill)
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_drop_shadow(
DropShadow::new_with_standard_offset_and_spread(shadow_color)
.with_offset(Vector2F::zero()),
)
.with_background(theme.background().into_solid())
.finish()
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod clone_repo_view;
pub mod create_project_view;
pub mod glowing_editor;
pub mod project_buttons;
@@ -0,0 +1,293 @@
use std::borrow::Cow;
use pathfinder_color::ColorU;
use pathfinder_geometry::vector::Vector2F;
use warp_core::{
features::FeatureFlag,
ui::{appearance::Appearance, color::blend::Blend as _, theme::color::internal_colors, Icon},
};
use warpui::{
elements::{
ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow,
Expanded, Flex, Hoverable, MouseStateHandle, OffsetPositioning, ParentAnchor,
ParentElement as _, ParentOffsetBounds, Radius, Stack,
},
fonts::Weight,
keymap::EditableBinding,
platform::{file_picker::FilePickerError, Cursor, FilePickerConfiguration},
ui_components::components::{UiComponent as _, UiComponentStyles},
AppContext, Element, Entity, SingletonEntity as _, TypedActionView, View, ViewContext,
};
use crate::util::bindings::{keybinding_name_to_display_string, BindingGroup, CustomAction};
const BUTTON_MIN_WIDTH: f32 = 149.;
pub fn init(app: &mut AppContext) {
use warpui::keymap::macros::*;
app.register_editable_bindings([
EditableBinding::new(
"project_buttons:open_repository",
"Open repository",
ProjectButtonsAction::OpenRepository,
)
.with_context_predicate(id!("ProjectButons"))
.with_group(BindingGroup::Folders.as_str())
.with_custom_action(CustomAction::OpenRepository),
EditableBinding::new(
"project_buttons:create_new_project",
"Create new project",
ProjectButtonsAction::CreateProject,
)
.with_context_predicate(id!("ProjectButons"))
.with_enabled(|| FeatureFlag::CreateProjectFlow.is_enabled())
.with_mac_key_binding("cmd-shift-N")
.with_linux_or_windows_key_binding("alt-shift-N"),
]);
}
#[derive(Default)]
struct StateHandles {
open_repo_button: MouseStateHandle,
create_project_button: MouseStateHandle,
clone_repo_button: MouseStateHandle,
}
pub struct ProjectButtons {
state_handles: StateHandles,
}
struct TooltipData {
text: String,
keybinding: Option<String>,
}
impl ProjectButtons {
pub fn new(_ctx: &mut ViewContext<Self>) -> Self {
Self {
state_handles: Default::default(),
}
}
pub fn open_repository(ctx: &mut ViewContext<Self>) {
ctx.open_file_picker(
move |result, ctx| {
if let Some(path_result) = result.map(|paths| paths.into_iter().next()).transpose()
{
ctx.emit(ProjectButtonsEvent::OpenRepository(path_result));
}
},
FilePickerConfiguration::new().folders_only(),
);
}
fn glowing_button(
&self,
label_text: impl Into<Cow<'static, str>> + Clone,
icon: Icon,
action: ProjectButtonsAction,
tooltip: TooltipData,
mouse_state: MouseStateHandle,
app: &AppContext,
) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let icon_color = internal_colors::fg_overlay_6(theme);
let label = appearance
.ui_builder()
.paragraph(label_text.clone())
.with_style(UiComponentStyles {
font_weight: Some(Weight::Semibold),
..Default::default()
})
.build()
.finish();
Hoverable::new(mouse_state, move |state| {
let icon_el = Container::new(
ConstrainedBox::new(icon.to_warpui_icon(icon_color).finish())
.with_height(20.)
.with_width(20.)
.finish(),
)
.finish();
let vertical_content = Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(icon_el)
.with_child(Container::new(label).with_margin_top(4.).finish())
.finish();
let base = ConstrainedBox::new(
Container::new(
Container::new(vertical_content)
.with_uniform_padding(16.)
.finish(),
)
.with_border(theme.outline().into_solid())
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
.with_drop_shadow(
DropShadow::new_with_standard_offset_and_spread(ColorU::new(255, 143, 253, 15))
.with_offset(Vector2F::zero()),
)
.with_background(if state.is_hovered() {
theme
.background()
.blend(&theme.surface_overlay_1())
.into_solid()
} else {
theme.background().into_solid()
})
.finish(),
)
.with_min_width(BUTTON_MIN_WIDTH)
.finish();
// Optional tooltip with keybinding string
if state.is_hovered() {
let tooltip = if let Some(keybinding) = tooltip.keybinding {
appearance
.ui_builder()
.tool_tip_with_sublabel(tooltip.text, keybinding)
.build()
.finish()
} else {
appearance
.ui_builder()
.tool_tip(tooltip.text)
.build()
.finish()
};
let mut stack = Stack::new();
stack.add_child(base);
let offset = OffsetPositioning::offset_from_parent(
Vector2F::new(0., 4.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::BottomMiddle,
ChildAnchor::TopMiddle,
);
stack.add_positioned_overlay_child(tooltip, offset);
stack.finish()
} else {
base
}
})
.with_cursor(Cursor::PointingHand)
.on_click(move |ctx, _, _| ctx.dispatch_typed_action(action))
.finish()
}
}
pub enum ProjectButtonsEvent {
OpenRepository(Result<String, FilePickerError>),
CreateProject,
CloneRepository,
}
impl Entity for ProjectButtons {
type Event = ProjectButtonsEvent;
}
#[derive(Clone, Copy, Debug)]
pub enum ProjectButtonsAction {
OpenRepository,
CreateProject,
CloneRepository,
}
impl TypedActionView for ProjectButtons {
type Action = ProjectButtonsAction;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
ProjectButtonsAction::OpenRepository => Self::open_repository(ctx),
ProjectButtonsAction::CreateProject => ctx.emit(ProjectButtonsEvent::CreateProject),
ProjectButtonsAction::CloneRepository => ctx.emit(ProjectButtonsEvent::CloneRepository),
}
}
}
impl View for ProjectButtons {
fn ui_name() -> &'static str {
"ProjectButons"
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let mut row = Flex::row();
if FeatureFlag::CreateProjectFlow.is_enabled() {
row.add_children([
Container::new(self.glowing_button(
"Create new project",
Icon::Plus,
ProjectButtonsAction::CreateProject,
TooltipData {
text: "Create and initialize a brand new project".to_string(),
keybinding: keybinding_name_to_display_string(
"project_buttons:create_new_project",
app,
),
},
self.state_handles.create_project_button.clone(),
app,
))
.with_margin_right(16.)
.finish(),
Container::new(self.glowing_button(
"Open repository",
Icon::Folder,
ProjectButtonsAction::OpenRepository,
TooltipData {
text: "Open an existing local folder or repository".to_string(),
keybinding: keybinding_name_to_display_string(
"project_buttons:open_repository",
app,
),
},
self.state_handles.open_repo_button.clone(),
app,
))
.with_margin_right(16.)
.finish(),
self.glowing_button(
"Clone repository",
Icon::Duplicate,
ProjectButtonsAction::CloneRepository,
TooltipData {
text: "Clone a repo from GitHub or another source".to_string(),
keybinding: None,
},
self.state_handles.clone_repo_button.clone(),
app,
),
]);
} else {
row.add_child(
Expanded::new(
1.,
self.glowing_button(
"Open repository",
Icon::Plus,
ProjectButtonsAction::CreateProject,
TooltipData {
text: "Open an existing local folder or repository".to_string(),
keybinding: keybinding_name_to_display_string(
"project_buttons:open_repository",
app,
),
},
self.state_handles.create_project_button.clone(),
app,
),
)
.finish(),
);
}
row.finish()
}
}