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
+3
View File
@@ -0,0 +1,3 @@
mod view;
pub use view::*;
+856
View File
@@ -0,0 +1,856 @@
use std::collections::HashSet;
use std::ops::Deref as _;
use std::path::PathBuf;
use std::sync::Arc;
use chrono::Utc;
use itertools::Itertools as _;
use pathfinder_geometry::vector::vec2f;
use warp_core::context_flag::ContextFlag;
use warp_core::features::FeatureFlag;
use warpui::elements::{
Border, ChildView, Clipped, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox,
Container, CornerRadius, Fill, Flex, MainAxisAlignment, MainAxisSize, MouseStateHandle,
ParentElement, Radius, SavePosition, Shrinkable,
};
use warpui::platform::Cursor;
use warpui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
use warpui::ui_components::components::UiComponent as _;
use warpui::{
units::{IntoPixels, Pixels},
AppContext, Element, Entity, FocusContext, ModelHandle, SingletonEntity, TypedActionView,
ViewContext, ViewHandle,
};
use super::super::palette_styles as styles;
use crate::appearance::Appearance;
use crate::cloud_object::model::persistence::CloudModel;
use crate::drive::CloudObjectTypeAndId;
use crate::palette::PaletteMode;
use crate::pane_group::pane::welcome_view::WelcomeViewAction;
use crate::search::action::search_item::MatchedBinding;
use crate::search::action::{CommandBindingDataSource, Event as CommandBindingDataSourceEvent};
use crate::search::binding_source::BindingSource;
use crate::search::command_palette::conversations::{self};
use crate::search::command_palette::mixer::CommandPaletteItemAction;
use crate::search::command_palette::new_session::{AllowedSessionKinds, NewSessionDataSource};
use crate::search::command_palette::{launch_config, warp_drive, CommandPaletteMixer};
use crate::search::command_search::projects::project_data_source::ProjectDataSource;
use crate::search::command_search::projects::{ProjectSearchItem, SuggestedProjectsDataSource};
use crate::search::data_source::QueryResult;
use crate::search::mixer::{dedupe_score, DedupeStrategy};
use crate::search::result_renderer::QueryResultRenderer;
use crate::search::search_bar::{
SearchBar, SearchBarEvent, SearchBarState, SearchResultOrdering, SelectionUpdate,
};
use crate::search::QueryFilter;
use crate::send_telemetry_from_ctx;
use crate::server::{ids::SyncId, telemetry::TelemetryEvent};
use crate::settings::AISettings;
use crate::terminal::History;
use crate::themes::theme::WarpTheme;
use crate::ui_components::icons::Icon;
use crate::workflows::{WorkflowSelectionSource, WorkflowSource, WorkflowType};
use crate::workspace::WorkspaceAction;
/// Position ID for the command palette list.
const PALETTE_LIST_SAVE_POSITION_ID: &str = "welcome_palette:list";
/// Max number of results to be returned by the search mixer. We set this to an arbitrarily
/// large size to minimize performances issues caused by rendering the elements of the palette
/// using a [`ClippedScrollable`].
// TODO(alokedesai): Remove once we add a properly viewported element.
const MAX_SEARCH_RESULTS: usize = 250;
const MAX_PROJECTS_IN_ZERO_STATE: usize = 4;
const MAX_ITEMS_IN_ZERO_STATE: usize = 5;
#[derive(Debug)]
pub enum Action {
ResultClicked { action: CommandPaletteItemAction },
ParentAction { action: WelcomeViewAction },
Close,
}
pub enum Event {
Close,
ParentAction {
action: WelcomeViewAction,
},
/// Execute the workflow identified by `id`.
ExecuteWorkflow {
id: SyncId,
},
/// Invoke the env vars identified by `id`.
InvokeEnvironmentVariables {
id: SyncId,
},
/// Open a notebook identified by `id`.
OpenNotebook {
id: SyncId,
},
/// View the relevant object in the Warp Drive sidebar.
ViewInWarpDrive {
id: CloudObjectTypeAndId,
},
/// Open a file at the given path.
OpenFile {
path: String,
},
/// Open a directory at the given path.
OpenDirectory {
path: String,
},
NewConversationInProject {
path: String,
},
}
#[derive(Default)]
struct StateHandles {
clipped_scroll_state: ClippedScrollStateHandle,
open_project_button: MouseStateHandle,
terminal_session_button: MouseStateHandle,
}
#[derive(Copy, Clone, Debug)]
enum SelectedItem {
ListItem(usize),
OpenProjectButton,
TerminalSessionButton,
}
impl Default for SelectedItem {
fn default() -> Self {
Self::ListItem(0)
}
}
pub struct WelcomePalette {
startup_directory: Option<PathBuf>,
search_bar: ViewHandle<SearchBar<CommandPaletteItemAction>>,
search_bar_state: ModelHandle<SearchBarState<CommandPaletteItemAction>>,
state_handles: StateHandles,
/// Placeholder element to render when no results are found.
placeholder_query_renderer: QueryResultRenderer<CommandPaletteItemAction>,
binding_source: ModelHandle<BindingSource>,
zero_state_items: Vec<QueryResultRenderer<CommandPaletteItemAction>>,
selected_item: SelectedItem,
project_data_source: ModelHandle<ProjectDataSource>,
conversations_data_source: ModelHandle<conversations::DataSource>,
suggested_projects_data_source: ModelHandle<SuggestedProjectsDataSource>,
open_project_keybinding: Option<String>,
terminal_session_keybinding: Option<String>,
}
impl Entity for WelcomePalette {
type Event = Event;
}
impl TypedActionView for WelcomePalette {
type Action = Action;
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
match action {
Action::ResultClicked { action } => {
self.handle_result_accepted(action.clone(), ctx);
}
Action::ParentAction { action } => ctx.emit(Event::ParentAction { action: *action }),
Action::Close => self.close(ctx, None),
}
}
}
impl warpui::View for WelcomePalette {
fn ui_name() -> &'static str {
"WelcomePalette"
}
fn render(&self, app: &AppContext) -> Box<dyn Element> {
let appearance = Appearance::as_ref(app);
let theme = appearance.theme();
let mut palette = Flex::column();
palette.add_child(self.render_search_bar());
if self.search_bar_state.as_ref(app).should_show_zero_state() {
palette.add_child(
Shrinkable::new(
1.,
self.render_item_list(&self.zero_state_items, self.selected_item, theme, app),
)
.finish(),
);
palette.add_child(self.render_footer_buttons(self.selected_item, appearance));
} else {
palette.add_child(Shrinkable::new(1., self.render_palette_list(theme, app)).finish());
};
Container::new(
ConstrainedBox::new(palette.finish())
.with_width(styles::PALETTE_WIDTH)
.with_max_height(styles::PALETTE_HEIGHT)
.finish(),
)
.with_background(theme.surface_2())
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
.with_border(Border::all(1.0).with_border_fill(theme.outline()))
.with_padding_bottom(10.)
.with_drop_shadow(*styles::DROP_SHADOW)
.finish()
}
fn on_focus(&mut self, focus_ctx: &FocusContext, ctx: &mut ViewContext<Self>) {
if focus_ctx.is_self_focused() {
ctx.focus(&self.search_bar);
self.binding_source.update(ctx, |_, ctx| ctx.notify());
}
}
}
impl WelcomePalette {
pub fn new(
startup_directory: Option<PathBuf>,
binding_source: BindingSource,
open_project_keybinding: Option<String>,
terminal_session_keybinding: Option<String>,
ctx: &mut ViewContext<Self>,
) -> Self {
let search_bar_state = ctx.add_model(|_ctx| {
SearchBarState::new(SearchResultOrdering::TopDown).with_max_results(MAX_SEARCH_RESULTS)
});
ctx.subscribe_to_model(&search_bar_state, |me, _, event, ctx| {
me.handle_search_bar_event(event, ctx);
});
ctx.observe(&search_bar_state, |_, _, ctx| ctx.notify());
let binding_source = ctx.add_model(|_| binding_source);
let actions_data_source =
ctx.add_model(|ctx| CommandBindingDataSource::new(binding_source.clone(), ctx));
ctx.subscribe_to_model(&actions_data_source, Self::handle_actions_data_source_event);
let project_data_source = ctx.add_model(ProjectDataSource::new);
let suggested_projects_data_source = ctx.add_model(SuggestedProjectsDataSource::new);
let conversations_data_source = ctx.add_model(|_| conversations::DataSource::new());
let launch_config_data_source = ctx.add_model(launch_config::DataSource::new);
let new_session_data_source = ctx.add_model(|ctx| {
NewSessionDataSource::new(binding_source.clone(), ctx)
.with_allowed_kinds(AllowedSessionKinds::tabs_only())
});
let warp_drive_data_source = ctx.add_model(warp_drive::DataSource::new);
let mixer = ctx.add_model(|ctx| {
let mut mixer = CommandPaletteMixer::new();
mixer.add_sync_source(actions_data_source.clone(), HashSet::new());
mixer.add_sync_source(project_data_source.clone(), HashSet::new());
mixer.add_sync_source(suggested_projects_data_source.clone(), HashSet::new());
mixer.add_sync_source(warp_drive_data_source.clone(), HashSet::new());
if AISettings::as_ref(ctx).is_any_ai_enabled(ctx) {
mixer.add_sync_source(conversations_data_source.clone(), HashSet::new());
}
if ContextFlag::LaunchConfigurations.is_enabled() {
mixer.add_sync_source(launch_config_data_source.clone(), HashSet::new());
}
if FeatureFlag::ShellSelector.is_enabled() && cfg!(feature = "local_tty") {
mixer.add_sync_source(new_session_data_source.clone(), HashSet::new());
}
mixer.set_dedupe_strategy(DedupeStrategy::HighestScore);
mixer
});
let ui_font_family = Appearance::as_ref(ctx).ui_font_family();
let search_bar = ctx.add_typed_action_view(|ctx| {
SearchBar::new(
mixer.clone(),
search_bar_state.clone(),
"Code, build, or search for anything...",
Self::create_query_result_renderer,
ctx,
)
.with_font_family(ui_font_family, ctx)
});
ctx.subscribe_to_view(&search_bar, |me, _, event, ctx| {
me.handle_search_bar_event(event, ctx);
});
ctx.subscribe_to_model(&History::handle(ctx), |me, _model, _event, ctx| {
me.compute_zero_state_items(ctx);
ctx.notify();
});
let placeholder_element = QueryResultRenderer::new(
MatchedBinding::placeholder("No results found".into()).into(),
"welcome_palette:no_results".into(),
|_, _, _| {},
*styles::QUERY_RESULT_RENDERER_STYLES,
);
Self {
startup_directory,
search_bar,
search_bar_state,
state_handles: Default::default(),
placeholder_query_renderer: placeholder_element,
binding_source,
project_data_source,
conversations_data_source,
open_project_keybinding,
terminal_session_keybinding,
suggested_projects_data_source,
zero_state_items: Default::default(),
selected_item: Default::default(),
}
}
fn handle_actions_data_source_event(
&mut self,
_: ModelHandle<CommandBindingDataSource>,
event: &CommandBindingDataSourceEvent,
ctx: &mut ViewContext<Self>,
) {
#[allow(irrefutable_let_patterns)]
if let CommandBindingDataSourceEvent::IndexUpdated = event {
self.compute_zero_state_items(ctx);
}
}
fn compute_zero_state_items(&mut self, ctx: &mut ViewContext<Self>) {
let mut projects = self.project_data_source.read(ctx, |projects, ctx| {
projects
.top_n(MAX_PROJECTS_IN_ZERO_STATE, ctx)
.collect_vec()
});
// We want the first item to be the startup directory as if it were a project. If it
// already is a project, push it to the front. If not, create an item for it.
let startup_dir_idx = projects
.iter()
.find_position(|proj| Some(PathBuf::from(proj.path.clone())) == self.startup_directory);
if let Some((i, _)) = startup_dir_idx {
let item = projects.remove(i);
projects.insert(0, item);
} else if let Some(dir) = &self.startup_directory {
let startup_dir_item = ProjectSearchItem::new(
dir.to_string_lossy().into_owned(),
fuzzy_match::FuzzyMatchResult::no_match(),
Utc::now().naive_utc(),
);
projects.insert(0, startup_dir_item);
}
let suggestion_slots = MAX_PROJECTS_IN_ZERO_STATE.saturating_sub(projects.len());
let suggested_projects = if suggestion_slots > 0 {
self.suggested_projects_data_source
.read(ctx, |suggested, _ctx: &AppContext| {
suggested.top_n(suggestion_slots)
})
} else {
vec![]
};
let conversation_slots = MAX_ITEMS_IN_ZERO_STATE
.saturating_sub(projects.len())
.saturating_sub(suggested_projects.len());
let conversations = if conversation_slots > 0 {
self.conversations_data_source
.read(ctx, |conversations, ctx| {
conversations.top_n(conversation_slots, ctx).collect()
})
} else {
vec![]
};
let projects = dedupe_score(
projects
.into_iter()
.map(QueryResult::from)
.chain(suggested_projects)
.collect(),
);
self.zero_state_items = projects
.into_iter()
.chain(conversations)
.enumerate()
.map(|(i, item)| Self::create_query_result_renderer(i, item))
.collect();
if self.zero_state_items.is_empty() {
self.selected_item = SelectedItem::OpenProjectButton;
}
}
/// Set the active query filter in the search bar to be `filter`.
pub fn set_active_query_filter(&mut self, filter: QueryFilter, ctx: &mut ViewContext<Self>) {
self.search_bar.update(ctx, |view, ctx| {
view.set_visible_query_filter(Some((filter, filter.filter_atom().primary_text)), ctx)
});
ctx.notify();
}
pub fn select_next_item(&mut self, ctx: &mut ViewContext<Self>) {
self.search_bar_state.update(ctx, |state, ctx| {
state.handle_selection_update(SelectionUpdate::Down, ctx);
});
ctx.notify();
}
pub fn select_prev_item(&mut self, ctx: &mut ViewContext<Self>) {
self.search_bar_state.update(ctx, |state, ctx| {
state.handle_selection_update(SelectionUpdate::Up, ctx);
});
ctx.notify();
}
pub fn active_query_filter(&self, app: &AppContext) -> Option<QueryFilter> {
self.search_bar_state
.as_ref(app)
.active_visible_query_filter()
}
pub fn is_mode_enabled(&self, mode: PaletteMode, app: &AppContext) -> bool {
let Some(active_query_filter) = self.active_query_filter(app) else {
return false;
};
matches!(
(mode, active_query_filter),
(PaletteMode::Command, QueryFilter::Actions)
| (PaletteMode::Navigation, QueryFilter::Sessions)
| (PaletteMode::LaunchConfig, QueryFilter::LaunchConfigurations)
)
}
fn create_query_result_renderer(
index: usize,
result: QueryResult<CommandPaletteItemAction>,
) -> QueryResultRenderer<CommandPaletteItemAction> {
QueryResultRenderer::new(
result,
Self::query_result_save_position_id(index),
|_, action, event_ctx| {
event_ctx.dispatch_typed_action(Action::ResultClicked { action })
},
*styles::QUERY_RESULT_RENDERER_STYLES,
)
}
/// Returns the position ID for a query result at `index`.
fn query_result_save_position_id(index: usize) -> String {
format!("welcome_palette:query_result:{index}")
}
fn render_search_bar(&self) -> Box<dyn Element> {
Container::new(
ConstrainedBox::new(Clipped::new(ChildView::new(&self.search_bar).finish()).finish())
.finish(),
)
.with_vertical_padding(styles::SEARCH_BAR_PADDING_VERTICAL)
.with_horizontal_padding(styles::RESULT_PADDING_HORIZONTAL)
.finish()
}
/// Handles events emitted by the search bar.
fn handle_search_bar_event(
&mut self,
event: &SearchBarEvent<CommandPaletteItemAction>,
ctx: &mut ViewContext<Self>,
) {
match event {
SearchBarEvent::Close => {
self.close(ctx, None);
}
SearchBarEvent::BufferCleared { .. } => {}
SearchBarEvent::ResultAccepted { action, .. } => {
self.handle_result_accepted(action.clone(), ctx);
}
SearchBarEvent::ResultSelected { index } => {
self.scroll_selected_index_into_view(*index, ctx);
ctx.notify();
}
SearchBarEvent::QueryFilterChanged { .. } => {}
SearchBarEvent::SelectionUpdateInZeroState { selection_update } => {
match selection_update {
SelectionUpdate::Up => {
self.selected_item = match self.selected_item {
SelectedItem::ListItem(i) => {
SelectedItem::ListItem(i.saturating_sub(1))
}
SelectedItem::OpenProjectButton => {
SelectedItem::ListItem(self.zero_state_items.len() - 1)
}
SelectedItem::TerminalSessionButton => SelectedItem::OpenProjectButton,
};
ctx.notify();
}
SelectionUpdate::Down => {
self.selected_item = match self.selected_item {
SelectedItem::ListItem(i) => {
if i == self.zero_state_items.len() - 1 {
SelectedItem::OpenProjectButton
} else {
SelectedItem::ListItem(i.saturating_add(1))
}
}
SelectedItem::OpenProjectButton
| SelectedItem::TerminalSessionButton => {
SelectedItem::TerminalSessionButton
}
};
ctx.notify();
}
SelectionUpdate::Clear | SelectionUpdate::Top => {
self.selected_item = SelectedItem::ListItem(0);
ctx.notify();
}
SelectionUpdate::Bottom => {
self.selected_item = SelectedItem::TerminalSessionButton;
ctx.notify();
}
}
}
SearchBarEvent::EnterInZeroState { modified_enter } => match self.selected_item {
SelectedItem::ListItem(i) => {
let Some(query_result) = self.zero_state_items.get(i) else {
return;
};
let action = if *modified_enter {
query_result.search_result.execute_result()
} else {
query_result.search_result.accept_result()
};
self.handle_result_accepted(action, ctx);
}
SelectedItem::OpenProjectButton => ctx.emit(Event::ParentAction {
action: WelcomeViewAction::OpenProject,
}),
SelectedItem::TerminalSessionButton => ctx.emit(Event::ParentAction {
action: WelcomeViewAction::CreateTerminalSession,
}),
},
}
}
/// Scrolls the query result at `index` into view.
fn scroll_selected_index_into_view(&self, index: usize, ctx: &mut ViewContext<Self>) {
let list_bounds = ctx.element_position_by_id(PALETTE_LIST_SAVE_POSITION_ID);
let item_bounds =
ctx.element_position_by_id(Self::query_result_save_position_id(index).as_str());
let Some((viewport_bounds, position_size)) = list_bounds.zip(item_bounds) else {
return;
};
// If the selected index is contained within the viewport, there is no need to change the
// scroll position.
if viewport_bounds.contains_rect(position_size) {
return;
}
let scroll_delta = if position_size.max_y() > viewport_bounds.max_y() {
// The item is below the viewport. Update the scroll position by the number of pixels
// the bottom of the item is below the viewport.
position_size.max_y() - viewport_bounds.max_y()
} else {
// The item is above the viewport. Update the scroll position by the number of pixels
// the top of the item is above the viewport.
position_size.min_y() - viewport_bounds.min_y()
};
let scroll_top = self.state_handles.clipped_scroll_state.scroll_start();
self.state_handles
.clipped_scroll_state
.scroll_to(scroll_top + scroll_delta.into_pixels());
ctx.notify();
}
fn close(&mut self, ctx: &mut ViewContext<Self>, accepted_action_type: Option<&'static str>) {
let buffer_length = self.search_bar.as_ref(ctx).query(ctx).len();
let filter = self.active_query_filter(ctx);
let event = if let Some(result_type) = accepted_action_type {
TelemetryEvent::PaletteSearchResultAccepted {
result_type,
filter,
buffer_length,
}
} else {
TelemetryEvent::PaletteSearchExited {
filter,
buffer_length,
}
};
send_telemetry_from_ctx!(event, ctx);
self.state_handles.clipped_scroll_state = Default::default();
self.reset(ctx);
// Some of the actions that are dispatched before closing can close the Window (e.g. "Close
// Tab" on the final tab of the window). Confirm that the Window still exists before trying
// to update the view.
if ctx.root_view_id(ctx.window_id()).is_some() {
ctx.emit(Event::Close);
}
}
pub fn reset(&mut self, ctx: &mut ViewContext<Self>) {
self.state_handles
.clipped_scroll_state
.scroll_to(Pixels::zero());
self.search_bar.update(ctx, |search_bar, ctx| {
search_bar.reset(
None, /* initial_query */
None, /* query_filter */
SearchResultOrdering::TopDown,
ctx,
)
});
ctx.notify();
}
/// Inserts `query` into the search bar.
pub fn insert_query_text(&mut self, query: &str, ctx: &mut ViewContext<Self>) {
self.search_bar.update(ctx, |search_bar, ctx| {
search_bar.insert_query_text(query, ctx);
})
}
fn render_palette_list(&self, theme: &WarpTheme, app: &AppContext) -> Box<dyn Element> {
match self.search_bar_state.as_ref(app).query_result_renderers() {
None => {
self.placeholder_query_renderer
.render(0, true /* is_selected */, app)
}
Some(renderers) if renderers.is_empty() => {
self.placeholder_query_renderer
.render(0, true /* is_selected */, app)
}
Some(renderers) => {
let selected_index = self.search_bar_state.as_ref(app).selected_index();
self.render_item_list(
renderers,
SelectedItem::ListItem(selected_index.unwrap_or_default()),
theme,
app,
)
}
}
}
fn render_item_list(
&self,
renderers: &[QueryResultRenderer<CommandPaletteItemAction>],
selected_item: SelectedItem,
theme: &WarpTheme,
app: &AppContext,
) -> Box<dyn Element> {
let selected_index = match selected_item {
SelectedItem::ListItem(i) => Some(i),
_ => None,
};
let list = Flex::column()
.with_children(renderers.iter().enumerate().map(|(index, renderer)| {
SavePosition::new(
renderer.render(index, Some(index) == selected_index, app),
renderer.position_id.as_str(),
)
.finish()
}))
.finish();
SavePosition::new(
ClippedScrollable::vertical(
self.state_handles.clipped_scroll_state.clone(),
list,
styles::SCROLLBAR_WIDTH,
theme.nonactive_ui_detail().into(),
theme.active_ui_detail().into(),
// Leave the scrollbar gutter background transparent.
Fill::None,
)
.with_overlayed_scrollbar()
.finish(),
PALETTE_LIST_SAVE_POSITION_ID,
)
.finish()
}
fn render_footer_buttons(
&self,
selected_item: SelectedItem,
appearance: &Appearance,
) -> Box<dyn Element> {
let theme = appearance.theme();
let mut open_project_button = appearance
.ui_builder()
.button(
ButtonVariant::Basic,
self.state_handles.open_project_button.clone(),
)
.with_text_and_icon_label(TextAndIcon::new(
TextAndIconAlignment::IconFirst,
match &self.open_project_keybinding {
Some(keystroke) => format!("Add repository {keystroke}"),
None => "Add repository".to_string(),
},
Icon::Plus.to_warpui_icon(theme.foreground()),
MainAxisSize::Max,
MainAxisAlignment::Center,
vec2f(16., 16.),
))
.with_cursor(Some(Cursor::PointingHand));
if let SelectedItem::OpenProjectButton = selected_item {
let style = *open_project_button.hovered_styles();
open_project_button = open_project_button.with_style(style);
}
let mut terminal_session_button = appearance
.ui_builder()
.button(
ButtonVariant::Basic,
self.state_handles.terminal_session_button.clone(),
)
.with_text_and_icon_label(TextAndIcon::new(
TextAndIconAlignment::IconFirst,
match &self.terminal_session_keybinding {
Some(keystroke) => format!("Terminal session {keystroke}"),
None => "Terminal session".to_string(),
},
Icon::Terminal.to_warpui_icon(theme.foreground()),
MainAxisSize::Max,
MainAxisAlignment::Center,
vec2f(16., 16.),
))
.with_cursor(Some(Cursor::PointingHand));
if let SelectedItem::TerminalSessionButton = selected_item {
let style = *terminal_session_button.hovered_styles();
terminal_session_button = terminal_session_button.with_style(style);
}
let row = Flex::row().with_children([
Shrinkable::new(
1.,
Container::new(
open_project_button
.build()
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(Action::ParentAction {
action: WelcomeViewAction::OpenProject,
});
})
.finish(),
)
.with_margin_right(4.)
.finish(),
)
.finish(),
Shrinkable::new(
1.,
Container::new(
terminal_session_button
.build()
.on_click(|ctx, _, _| {
ctx.dispatch_typed_action(Action::ParentAction {
action: WelcomeViewAction::CreateTerminalSession,
});
})
.finish(),
)
.with_padding_left(4.)
.finish(),
)
.finish(),
]);
Container::new(row.finish())
.with_padding_left(8.)
.with_padding_right(8.)
.with_margin_top(16.)
.finish()
}
/// Handles the `WelcomePaletteItemAction` action and closes the search panel.
fn handle_result_accepted(
&mut self,
result_action: CommandPaletteItemAction,
ctx: &mut ViewContext<Self>,
) {
match &result_action {
CommandPaletteItemAction::AcceptBinding { binding } => {
if let Some(action) = binding.action.as_deref() {
self.dispatch_typed_action_on_view(action, ctx);
};
}
CommandPaletteItemAction::NewConversationInProject {
path,
project_name: _,
} => {
ctx.emit(Event::NewConversationInProject { path: path.clone() });
}
CommandPaletteItemAction::NavigateToConversation { .. } => {
// This code is dead, so no need to support this case
}
CommandPaletteItemAction::OpenNotebook { id } => {
self.dispatch_typed_action_on_view(&WorkspaceAction::OpenNotebook { id: *id }, ctx);
self.close(ctx, Some(result_action.result_type()));
}
CommandPaletteItemAction::ExecuteWorkflow { id } => {
let Some(workflow) = CloudModel::as_ref(ctx).get_workflow(id) else {
log::warn!("Tried to execute workflow for id {id:?} but it does not exist");
return;
};
self.dispatch_typed_action_on_view(
&WorkspaceAction::RunWorkflow {
workflow: Arc::new(WorkflowType::Cloud(Box::new(workflow.clone()))),
workflow_source: WorkflowSource::Global,
workflow_selection_source: WorkflowSelectionSource::CommandPalette,
argument_override: None,
},
ctx,
);
self.close(ctx, Some(result_action.result_type()));
}
CommandPaletteItemAction::NewSession { source } => {
self.dispatch_typed_action_on_view(source.action().deref(), ctx);
self.close(ctx, Some(result_action.result_type()));
}
_ => {
// TODO
}
}
}
/// Dispatches `action` to the correct window and [`warpui::View`] by using the current state of
/// the [`BindingSource`] model.
fn dispatch_typed_action_on_view(
&self,
action: &dyn warpui::Action,
ctx: &mut ViewContext<Self>,
) {
send_telemetry_from_ctx!(
TelemetryEvent::SelectCommandPaletteOption(format!("{action:?}")),
ctx
);
let (window_id, view_id) = match self.binding_source.as_ref(ctx) {
BindingSource::View {
window_id, view_id, ..
} => (*window_id, *view_id),
BindingSource::None => return,
};
ctx.dispatch_typed_action_for_view(window_id, view_id, action);
}
}