diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index 6db774f6a1287..cacca012cb9bf 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -3,6 +3,7 @@ pub mod assistant_panel; mod context; mod context_editor; +mod context_history; pub mod context_store; mod inline_assistant; mod patch; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index b38516563eda7..4937d97873eb1 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,11 +1,12 @@ use crate::context_editor::{ ContextEditor, ContextEditorToolbarItem, ContextEditorToolbarItemEvent, DEFAULT_TAB_TITLE, }; +use crate::context_history::ContextHistory; use crate::{ slash_command::SlashCommandCompletionProvider, terminal_inline_assistant::TerminalInlineAssistant, Context, ContextId, ContextStore, ContextStoreEvent, DeployHistory, DeployPromptLibrary, InlineAssistant, InsertDraggedFiles, - NewContext, RemoteContextMetadata, SavedContextMetadata, ToggleFocus, ToggleModelSelector, + NewContext, ToggleFocus, ToggleModelSelector, }; use anyhow::Result; use assistant_settings::{AssistantDockPosition, AssistantSettings}; @@ -26,7 +27,6 @@ use language_model::{ LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, }; use language_model_selector::LanguageModelSelector; -use picker::{Picker, PickerDelegate}; use project::lsp_store::LocalLspAdapterDelegate; use project::Project; use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary}; @@ -35,12 +35,7 @@ use settings::{update_settings_file, Settings}; use smol::stream::StreamExt; use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; -use ui::{ - prelude::*, - utils::{format_distance_from_now, DateTimeType}, - Avatar, ContextMenu, ElevationIndex, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, - Tooltip, -}; +use ui::{prelude::*, ContextMenu, ElevationIndex, PopoverMenu, PopoverMenuHandle, Tooltip}; use util::{maybe, ResultExt}; use workspace::DraggedTab; use workspace::{ @@ -106,165 +101,11 @@ pub struct AssistantPanel { pub(crate) show_zed_ai_notice: bool, } -#[derive(Clone)] -enum ContextMetadata { - Remote(RemoteContextMetadata), - Saved(SavedContextMetadata), -} - -struct SavedContextPickerDelegate { - store: Model, - project: Model, - matches: Vec, - selected_index: usize, -} - -enum SavedContextPickerEvent { - Confirmed(ContextMetadata), -} - enum InlineAssistTarget { Editor(View, bool), Terminal(View), } -impl EventEmitter for Picker {} - -impl SavedContextPickerDelegate { - fn new(project: Model, store: Model) -> Self { - Self { - project, - store, - matches: Vec::new(), - selected_index: 0, - } - } -} - -impl PickerDelegate for SavedContextPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { - self.selected_index = ix; - } - - fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { - "Search...".into() - } - - fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { - let search = self.store.read(cx).search(query, cx); - cx.spawn(|this, mut cx| async move { - let matches = search.await; - this.update(&mut cx, |this, cx| { - let host_contexts = this.delegate.store.read(cx).host_contexts(); - this.delegate.matches = host_contexts - .iter() - .cloned() - .map(ContextMetadata::Remote) - .chain(matches.into_iter().map(ContextMetadata::Saved)) - .collect(); - this.delegate.selected_index = 0; - cx.notify(); - }) - .ok(); - }) - } - - fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { - if let Some(metadata) = self.matches.get(self.selected_index) { - cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone())); - } - } - - fn dismissed(&mut self, _cx: &mut ViewContext>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - cx: &mut ViewContext>, - ) -> Option { - let context = self.matches.get(ix)?; - let item = match context { - ContextMetadata::Remote(context) => { - let host_user = self.project.read(cx).host().and_then(|collaborator| { - self.project - .read(cx) - .user_store() - .read(cx) - .get_cached_user(collaborator.user_id) - }); - div() - .flex() - .w_full() - .justify_between() - .gap_2() - .child( - h_flex().flex_1().overflow_x_hidden().child( - Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into())) - .size(LabelSize::Small), - ), - ) - .child( - h_flex() - .gap_2() - .children(if let Some(host_user) = host_user { - vec![ - Avatar::new(host_user.avatar_uri.clone()).into_any_element(), - Label::new(format!("Shared by @{}", host_user.github_login)) - .color(Color::Muted) - .size(LabelSize::Small) - .into_any_element(), - ] - } else { - vec![Label::new("Shared by host") - .color(Color::Muted) - .size(LabelSize::Small) - .into_any_element()] - }), - ) - } - ContextMetadata::Saved(context) => div() - .flex() - .w_full() - .justify_between() - .gap_2() - .child( - h_flex() - .flex_1() - .child(Label::new(context.title.clone()).size(LabelSize::Small)) - .overflow_x_hidden(), - ) - .child( - Label::new(format_distance_from_now( - DateTimeType::Local(context.mtime), - false, - true, - true, - )) - .color(Color::Muted) - .size(LabelSize::Small), - ), - }; - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(item), - ) - } -} - impl AssistantPanel { pub fn load( workspace: WeakView, @@ -1175,7 +1016,7 @@ impl AssistantPanel { Some(self.active_context_editor(cx)?.read(cx).context.clone()) } - fn open_saved_context( + pub fn open_saved_context( &mut self, path: PathBuf, cx: &mut ViewContext, @@ -1463,66 +1304,6 @@ impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist { } } -pub struct ContextHistory { - picker: View>, - _subscriptions: Vec, - assistant_panel: WeakView, -} - -impl ContextHistory { - fn new( - project: Model, - context_store: Model, - assistant_panel: WeakView, - cx: &mut ViewContext, - ) -> Self { - let picker = cx.new_view(|cx| { - Picker::uniform_list( - SavedContextPickerDelegate::new(project, context_store.clone()), - cx, - ) - .modal(false) - .max_height(None) - }); - - let _subscriptions = vec![ - cx.observe(&context_store, |this, _, cx| { - this.picker.update(cx, |picker, cx| picker.refresh(cx)); - }), - cx.subscribe(&picker, Self::handle_picker_event), - ]; - - Self { - picker, - _subscriptions, - assistant_panel, - } - } - - fn handle_picker_event( - &mut self, - _: View>, - event: &SavedContextPickerEvent, - cx: &mut ViewContext, - ) { - let SavedContextPickerEvent::Confirmed(context) = event; - self.assistant_panel - .update(cx, |assistant_panel, cx| match context { - ContextMetadata::Remote(metadata) => { - assistant_panel - .open_remote_context(metadata.id.clone(), cx) - .detach_and_log_err(cx); - } - ContextMetadata::Saved(metadata) => { - assistant_panel - .open_saved_context(metadata.path.clone(), cx) - .detach_and_log_err(cx); - } - }) - .ok(); - } -} - #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum WorkflowAssistStatus { Pending, @@ -1531,28 +1312,6 @@ pub enum WorkflowAssistStatus { Idle, } -impl Render for ContextHistory { - fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - div().size_full().child(self.picker.clone()) - } -} - -impl FocusableView for ContextHistory { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter<()> for ContextHistory {} - -impl Item for ContextHistory { - type Event = (); - - fn tab_content_text(&self, _cx: &WindowContext) -> Option { - Some("History".into()) - } -} - pub struct ConfigurationView { focus_handle: FocusHandle, configuration_views: HashMap, diff --git a/crates/assistant/src/context_history.rs b/crates/assistant/src/context_history.rs new file mode 100644 index 0000000000000..9d2dc74e6027e --- /dev/null +++ b/crates/assistant/src/context_history.rs @@ -0,0 +1,249 @@ +use std::sync::Arc; + +use gpui::{ + AppContext, EventEmitter, FocusHandle, FocusableView, Model, Subscription, Task, View, WeakView, +}; +use picker::{Picker, PickerDelegate}; +use project::Project; +use ui::utils::{format_distance_from_now, DateTimeType}; +use ui::{prelude::*, Avatar, ListItem, ListItemSpacing}; +use workspace::Item; + +use crate::context_editor::DEFAULT_TAB_TITLE; +use crate::{AssistantPanel, ContextStore, RemoteContextMetadata, SavedContextMetadata}; + +#[derive(Clone)] +pub enum ContextMetadata { + Remote(RemoteContextMetadata), + Saved(SavedContextMetadata), +} + +enum SavedContextPickerEvent { + Confirmed(ContextMetadata), +} + +pub struct ContextHistory { + picker: View>, + _subscriptions: Vec, + assistant_panel: WeakView, +} + +impl ContextHistory { + pub fn new( + project: Model, + context_store: Model, + assistant_panel: WeakView, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.new_view(|cx| { + Picker::uniform_list( + SavedContextPickerDelegate::new(project, context_store.clone()), + cx, + ) + .modal(false) + .max_height(None) + }); + + let _subscriptions = vec![ + cx.observe(&context_store, |this, _, cx| { + this.picker.update(cx, |picker, cx| picker.refresh(cx)); + }), + cx.subscribe(&picker, Self::handle_picker_event), + ]; + + Self { + picker, + _subscriptions, + assistant_panel, + } + } + + fn handle_picker_event( + &mut self, + _: View>, + event: &SavedContextPickerEvent, + cx: &mut ViewContext, + ) { + let SavedContextPickerEvent::Confirmed(context) = event; + self.assistant_panel + .update(cx, |assistant_panel, cx| match context { + ContextMetadata::Remote(metadata) => { + assistant_panel + .open_remote_context(metadata.id.clone(), cx) + .detach_and_log_err(cx); + } + ContextMetadata::Saved(metadata) => { + assistant_panel + .open_saved_context(metadata.path.clone(), cx) + .detach_and_log_err(cx); + } + }) + .ok(); + } +} + +impl Render for ContextHistory { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div().size_full().child(self.picker.clone()) + } +} + +impl FocusableView for ContextHistory { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter<()> for ContextHistory {} + +impl Item for ContextHistory { + type Event = (); + + fn tab_content_text(&self, _cx: &WindowContext) -> Option { + Some("History".into()) + } +} + +struct SavedContextPickerDelegate { + store: Model, + project: Model, + matches: Vec, + selected_index: usize, +} + +impl EventEmitter for Picker {} + +impl SavedContextPickerDelegate { + fn new(project: Model, store: Model) -> Self { + Self { + project, + store, + matches: Vec::new(), + selected_index: 0, + } + } +} + +impl PickerDelegate for SavedContextPickerDelegate { + type ListItem = ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Search...".into() + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { + let search = self.store.read(cx).search(query, cx); + cx.spawn(|this, mut cx| async move { + let matches = search.await; + this.update(&mut cx, |this, cx| { + let host_contexts = this.delegate.store.read(cx).host_contexts(); + this.delegate.matches = host_contexts + .iter() + .cloned() + .map(ContextMetadata::Remote) + .chain(matches.into_iter().map(ContextMetadata::Saved)) + .collect(); + this.delegate.selected_index = 0; + cx.notify(); + }) + .ok(); + }) + } + + fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext>) { + if let Some(metadata) = self.matches.get(self.selected_index) { + cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone())); + } + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Option { + let context = self.matches.get(ix)?; + let item = match context { + ContextMetadata::Remote(context) => { + let host_user = self.project.read(cx).host().and_then(|collaborator| { + self.project + .read(cx) + .user_store() + .read(cx) + .get_cached_user(collaborator.user_id) + }); + div() + .flex() + .w_full() + .justify_between() + .gap_2() + .child( + h_flex().flex_1().overflow_x_hidden().child( + Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into())) + .size(LabelSize::Small), + ), + ) + .child( + h_flex() + .gap_2() + .children(if let Some(host_user) = host_user { + vec![ + Avatar::new(host_user.avatar_uri.clone()).into_any_element(), + Label::new(format!("Shared by @{}", host_user.github_login)) + .color(Color::Muted) + .size(LabelSize::Small) + .into_any_element(), + ] + } else { + vec![Label::new("Shared by host") + .color(Color::Muted) + .size(LabelSize::Small) + .into_any_element()] + }), + ) + } + ContextMetadata::Saved(context) => div() + .flex() + .w_full() + .justify_between() + .gap_2() + .child( + h_flex() + .flex_1() + .child(Label::new(context.title.clone()).size(LabelSize::Small)) + .overflow_x_hidden(), + ) + .child( + Label::new(format_distance_from_now( + DateTimeType::Local(context.mtime), + false, + true, + true, + )) + .color(Color::Muted) + .size(LabelSize::Small), + ), + }; + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(item), + ) + } +}