Improve UX for saved contexts (#12721)
Release Notes: - Added search for saved contexts. - Fixed a bug that caused titles generate by the LLM to be longer than one line.
This commit is contained in:
parent
9a5b97db00
commit
70ce06cb95
4 changed files with 387 additions and 225 deletions
|
@ -1,11 +1,11 @@
|
||||||
pub mod assistant_panel;
|
pub mod assistant_panel;
|
||||||
pub mod assistant_settings;
|
pub mod assistant_settings;
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
|
mod conversation_store;
|
||||||
mod inline_assistant;
|
mod inline_assistant;
|
||||||
mod model_selector;
|
mod model_selector;
|
||||||
mod prompt_library;
|
mod prompt_library;
|
||||||
mod prompts;
|
mod prompts;
|
||||||
mod saved_conversation;
|
|
||||||
mod search;
|
mod search;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
mod streaming_diff;
|
mod streaming_diff;
|
||||||
|
@ -17,10 +17,10 @@ use assistant_slash_command::SlashCommandRegistry;
|
||||||
use client::{proto, Client};
|
use client::{proto, Client};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
pub(crate) use completion_provider::*;
|
pub(crate) use completion_provider::*;
|
||||||
|
pub(crate) use conversation_store::*;
|
||||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||||
pub(crate) use inline_assistant::*;
|
pub(crate) use inline_assistant::*;
|
||||||
pub(crate) use model_selector::*;
|
pub(crate) use model_selector::*;
|
||||||
pub(crate) use saved_conversation::*;
|
|
||||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
|
@ -6,10 +6,10 @@ use crate::{
|
||||||
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
default_command::DefaultSlashCommand, SlashCommandCompletionProvider, SlashCommandLine,
|
||||||
SlashCommandRegistry,
|
SlashCommandRegistry,
|
||||||
},
|
},
|
||||||
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, CycleMessageRole, InlineAssist,
|
ApplyEdit, Assist, CompletionProvider, ConfirmCommand, ConversationStore, CycleMessageRole,
|
||||||
InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
|
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, MessageId,
|
||||||
MessageStatus, ModelSelector, QuoteSelection, ResetKey, Role, SavedConversation,
|
MessageMetadata, MessageStatus, ModelSelector, QuoteSelection, ResetKey, Role,
|
||||||
SavedConversationMetadata, SavedMessage, Split, ToggleFocus, ToggleHistory,
|
SavedConversation, SavedConversationMetadata, SavedMessage, Split, ToggleFocus, ToggleHistory,
|
||||||
ToggleModelSelector,
|
ToggleModelSelector,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
@ -29,17 +29,18 @@ use fs::Fs;
|
||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, point, rems, uniform_list, Action, AnyElement, AnyView, AppContext, AsyncAppContext,
|
div, point, rems, Action, AnyElement, AnyView, AppContext, AsyncAppContext, AsyncWindowContext,
|
||||||
AsyncWindowContext, ClipboardItem, Context, Empty, EventEmitter, FocusHandle, FocusableView,
|
ClipboardItem, Context, Empty, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
||||||
InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
|
IntoElement, Model, ModelContext, ParentElement, Pixels, Render, SharedString,
|
||||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle,
|
StatefulInteractiveElement, Styled, Subscription, Task, UpdateGlobal, View, ViewContext,
|
||||||
UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
|
VisualContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
language_settings::SoftWrap, AnchorRangeExt, AutoindentMode, Buffer, LanguageRegistry,
|
||||||
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
LspAdapterDelegate, OffsetRangeExt as _, Point, ToOffset as _,
|
||||||
};
|
};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
|
use project::{Project, ProjectLspAdapterDelegate, ProjectTransaction};
|
||||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -54,8 +55,8 @@ use std::{
|
||||||
};
|
};
|
||||||
use telemetry_events::AssistantKind;
|
use telemetry_events::AssistantKind;
|
||||||
use ui::{
|
use ui::{
|
||||||
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding,
|
popover_menu, prelude::*, ButtonLike, ContextMenu, ElevationIndex, KeyBinding, ListItem,
|
||||||
PopoverMenuHandle, Tab, TabBar, Tooltip,
|
ListItemSpacing, PopoverMenuHandle, Tab, TabBar, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -93,8 +94,8 @@ pub struct AssistantPanel {
|
||||||
height: Option<Pixels>,
|
height: Option<Pixels>,
|
||||||
active_conversation_editor: Option<ActiveConversationEditor>,
|
active_conversation_editor: Option<ActiveConversationEditor>,
|
||||||
show_saved_conversations: bool,
|
show_saved_conversations: bool,
|
||||||
saved_conversations: Vec<SavedConversationMetadata>,
|
conversation_store: Model<ConversationStore>,
|
||||||
saved_conversations_scroll_handle: UniformListScrollHandle,
|
saved_conversation_picker: View<Picker<SavedConversationPickerDelegate>>,
|
||||||
zoomed: bool,
|
zoomed: bool,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
toolbar: View<Toolbar>,
|
toolbar: View<Toolbar>,
|
||||||
|
@ -103,11 +104,102 @@ pub struct AssistantPanel {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
_watch_saved_conversations: Task<Result<()>>,
|
|
||||||
authentication_prompt: Option<AnyView>,
|
authentication_prompt: Option<AnyView>,
|
||||||
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
model_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SavedConversationPickerDelegate {
|
||||||
|
store: Model<ConversationStore>,
|
||||||
|
matches: Vec<SavedConversationMetadata>,
|
||||||
|
selected_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SavedConversationPickerEvent {
|
||||||
|
Confirmed { path: PathBuf },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<SavedConversationPickerEvent> for Picker<SavedConversationPickerDelegate> {}
|
||||||
|
|
||||||
|
impl SavedConversationPickerDelegate {
|
||||||
|
fn new(store: Model<ConversationStore>) -> Self {
|
||||||
|
Self {
|
||||||
|
store,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for SavedConversationPickerDelegate {
|
||||||
|
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<Picker<Self>>) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
|
||||||
|
"Search...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> 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| {
|
||||||
|
this.delegate.matches = matches;
|
||||||
|
this.delegate.selected_index = 0;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
|
if let Some(metadata) = self.matches.get(self.selected_index) {
|
||||||
|
cx.emit(SavedConversationPickerEvent::Confirmed {
|
||||||
|
path: metadata.path.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let conversation = self.matches.get(ix)?;
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.spacing(ListItemSpacing::Sparse)
|
||||||
|
.selected(selected)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
|
||||||
|
.color(Color::Muted)
|
||||||
|
.size(LabelSize::Small),
|
||||||
|
)
|
||||||
|
.child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ActiveConversationEditor {
|
struct ActiveConversationEditor {
|
||||||
editor: View<ConversationEditor>,
|
editor: View<ConversationEditor>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
@ -120,35 +212,14 @@ impl AssistantPanel {
|
||||||
) -> Task<Result<View<Self>>> {
|
) -> Task<Result<View<Self>>> {
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
|
let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||||
let saved_conversations = SavedConversationMetadata::list(fs.clone())
|
let conversation_store = cx
|
||||||
.await
|
.update(|cx| ConversationStore::new(fs.clone(), cx))?
|
||||||
.log_err()
|
.await?;
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// TODO: deserialize state.
|
// TODO: deserialize state.
|
||||||
let workspace_handle = workspace.clone();
|
let workspace_handle = workspace.clone();
|
||||||
workspace.update(&mut cx, |workspace, cx| {
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
cx.new_view::<Self>(|cx| {
|
cx.new_view::<Self>(|cx| {
|
||||||
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
|
|
||||||
let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
|
|
||||||
let (mut events, _) = fs
|
|
||||||
.watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
|
|
||||||
.await;
|
|
||||||
while events.next().await.is_some() {
|
|
||||||
let saved_conversations = SavedConversationMetadata::list(fs.clone())
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
.unwrap_or_default();
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
this.saved_conversations = saved_conversations;
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
let toolbar = cx.new_view(|cx| {
|
let toolbar = cx.new_view(|cx| {
|
||||||
let mut toolbar = Toolbar::new();
|
let mut toolbar = Toolbar::new();
|
||||||
toolbar.set_can_navigate(false, cx);
|
toolbar.set_can_navigate(false, cx);
|
||||||
|
@ -156,6 +227,15 @@ impl AssistantPanel {
|
||||||
toolbar
|
toolbar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let saved_conversation_picker = cx.new_view(|cx| {
|
||||||
|
Picker::uniform_list(
|
||||||
|
SavedConversationPickerDelegate::new(conversation_store.clone()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.modal(false)
|
||||||
|
.max_height(None)
|
||||||
|
});
|
||||||
|
|
||||||
let focus_handle = cx.focus_handle();
|
let focus_handle = cx.focus_handle();
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
cx.on_focus_in(&focus_handle, Self::focus_in),
|
cx.on_focus_in(&focus_handle, Self::focus_in),
|
||||||
|
@ -169,6 +249,14 @@ impl AssistantPanel {
|
||||||
CompletionProvider::global(cx).settings_version();
|
CompletionProvider::global(cx).settings_version();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
cx.observe(&conversation_store, |this, _, cx| {
|
||||||
|
this.saved_conversation_picker
|
||||||
|
.update(cx, |picker, cx| picker.refresh(cx));
|
||||||
|
}),
|
||||||
|
cx.subscribe(
|
||||||
|
&saved_conversation_picker,
|
||||||
|
Self::handle_saved_conversation_picker_event,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
cx.observe_global::<FileIcons>(|_, cx| {
|
cx.observe_global::<FileIcons>(|_, cx| {
|
||||||
|
@ -180,8 +268,8 @@ impl AssistantPanel {
|
||||||
workspace: workspace_handle,
|
workspace: workspace_handle,
|
||||||
active_conversation_editor: None,
|
active_conversation_editor: None,
|
||||||
show_saved_conversations: false,
|
show_saved_conversations: false,
|
||||||
saved_conversations,
|
saved_conversation_picker,
|
||||||
saved_conversations_scroll_handle: Default::default(),
|
conversation_store,
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
toolbar,
|
toolbar,
|
||||||
|
@ -192,7 +280,6 @@ impl AssistantPanel {
|
||||||
width: None,
|
width: None,
|
||||||
height: None,
|
height: None,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_watch_saved_conversations,
|
|
||||||
authentication_prompt: None,
|
authentication_prompt: None,
|
||||||
model_menu_handle: PopoverMenuHandle::default(),
|
model_menu_handle: PopoverMenuHandle::default(),
|
||||||
}
|
}
|
||||||
|
@ -206,8 +293,10 @@ impl AssistantPanel {
|
||||||
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
|
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
if self.focus_handle.is_focused(cx) {
|
if self.focus_handle.is_focused(cx) {
|
||||||
if let Some(editor) = self.active_conversation_editor() {
|
if self.show_saved_conversations {
|
||||||
cx.focus_view(editor);
|
cx.focus_view(&self.saved_conversation_picker);
|
||||||
|
} else if let Some(conversation) = self.active_conversation_editor() {
|
||||||
|
cx.focus_view(conversation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,6 +340,20 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_saved_conversation_picker_event(
|
||||||
|
&mut self,
|
||||||
|
_picker: View<Picker<SavedConversationPickerDelegate>>,
|
||||||
|
event: &SavedConversationPickerEvent,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
SavedConversationPickerEvent::Confirmed { path } => {
|
||||||
|
self.open_conversation(path.clone(), cx)
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn inline_assist(
|
pub fn inline_assist(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &InlineAssist,
|
_: &InlineAssist,
|
||||||
|
@ -409,17 +512,29 @@ impl AssistantPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_history(&mut self, _: &ToggleHistory, cx: &mut ViewContext<Self>) {
|
fn toggle_history(&mut self, _: &ToggleHistory, cx: &mut ViewContext<Self>) {
|
||||||
self.show_saved_conversations = !self.show_saved_conversations;
|
if self.show_saved_conversations {
|
||||||
cx.notify();
|
self.hide_history(cx);
|
||||||
|
} else {
|
||||||
|
self.show_history(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_history(&mut self, cx: &mut ViewContext<Self>) {
|
fn show_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.focus_view(&self.saved_conversation_picker);
|
||||||
if !self.show_saved_conversations {
|
if !self.show_saved_conversations {
|
||||||
self.show_saved_conversations = true;
|
self.show_saved_conversations = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hide_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(editor) = self.active_conversation_editor() {
|
||||||
|
cx.focus_view(&editor);
|
||||||
|
self.show_saved_conversations = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
|
fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
|
||||||
let mut propagate = true;
|
let mut propagate = true;
|
||||||
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
|
@ -613,37 +728,10 @@ impl AssistantPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_saved_conversation(
|
|
||||||
&mut self,
|
|
||||||
index: usize,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let conversation = &self.saved_conversations[index];
|
|
||||||
let path = conversation.path.clone();
|
|
||||||
|
|
||||||
ButtonLike::new(index)
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
this.open_conversation(path.clone(), cx)
|
|
||||||
.detach_and_log_err(cx)
|
|
||||||
}))
|
|
||||||
.full_width()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.w_full()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
|
|
||||||
.color(Color::Muted)
|
|
||||||
.size(LabelSize::Small),
|
|
||||||
)
|
|
||||||
.child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||||
cx.focus(&self.focus_handle);
|
cx.focus(&self.focus_handle);
|
||||||
|
|
||||||
|
let saved_conversation = self.conversation_store.read(cx).load(path.clone(), cx);
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let workspace = self.workspace.clone();
|
let workspace = self.workspace.clone();
|
||||||
let slash_commands = self.slash_commands.clone();
|
let slash_commands = self.slash_commands.clone();
|
||||||
|
@ -658,7 +746,7 @@ impl AssistantPanel {
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let saved_conversation = SavedConversation::load(&path, fs.as_ref()).await?;
|
let saved_conversation = saved_conversation.await?;
|
||||||
let conversation = Conversation::deserialize(
|
let conversation = Conversation::deserialize(
|
||||||
saved_conversation,
|
saved_conversation,
|
||||||
path.clone(),
|
path.clone(),
|
||||||
|
@ -705,7 +793,13 @@ impl AssistantPanel {
|
||||||
.h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
|
.h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.px_2()
|
.px_2()
|
||||||
.child(Label::new(editor.read(cx).title(cx)).into_element())
|
.child(
|
||||||
|
div()
|
||||||
|
.id("title")
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click(cx.listener(|this, _, cx| this.hide_history(cx)))
|
||||||
|
.child(Label::new(editor.read(cx).title(cx))),
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
.end_child(
|
.end_child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -780,22 +874,10 @@ impl AssistantPanel {
|
||||||
})
|
})
|
||||||
.child(contents.flex_1().child(
|
.child(contents.flex_1().child(
|
||||||
if self.show_saved_conversations || self.active_conversation_editor().is_none() {
|
if self.show_saved_conversations || self.active_conversation_editor().is_none() {
|
||||||
let view = cx.view().clone();
|
div()
|
||||||
let scroll_handle = self.saved_conversations_scroll_handle.clone();
|
.size_full()
|
||||||
let conversation_count = self.saved_conversations.len();
|
.child(self.saved_conversation_picker.clone())
|
||||||
uniform_list(
|
.into_any_element()
|
||||||
view,
|
|
||||||
"saved_conversations",
|
|
||||||
conversation_count,
|
|
||||||
|this, range, cx| {
|
|
||||||
range
|
|
||||||
.map(|ix| this.render_saved_conversation(ix, cx))
|
|
||||||
.collect()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.size_full()
|
|
||||||
.track_scroll(scroll_handle)
|
|
||||||
.into_any_element()
|
|
||||||
} else if let Some(editor) = self.active_conversation_editor() {
|
} else if let Some(editor) = self.active_conversation_editor() {
|
||||||
let editor = editor.clone();
|
let editor = editor.clone();
|
||||||
div()
|
div()
|
||||||
|
@ -1809,11 +1891,10 @@ impl Conversation {
|
||||||
|
|
||||||
let messages = self
|
let messages = self
|
||||||
.messages(cx)
|
.messages(cx)
|
||||||
.take(2)
|
|
||||||
.map(|message| message.to_request_message(self.buffer.read(cx)))
|
.map(|message| message.to_request_message(self.buffer.read(cx)))
|
||||||
.chain(Some(LanguageModelRequestMessage {
|
.chain(Some(LanguageModelRequestMessage {
|
||||||
role: Role::User,
|
role: Role::User,
|
||||||
content: "Summarize the conversation into a short title without punctuation"
|
content: "Summarize the conversation into a short title without punctuation."
|
||||||
.into(),
|
.into(),
|
||||||
}));
|
}));
|
||||||
let request = LanguageModelRequest {
|
let request = LanguageModelRequest {
|
||||||
|
@ -1830,13 +1911,17 @@ impl Conversation {
|
||||||
|
|
||||||
while let Some(message) = messages.next().await {
|
while let Some(message) = messages.next().await {
|
||||||
let text = message?;
|
let text = message?;
|
||||||
|
let mut lines = text.lines();
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.summary
|
let summary = this.summary.get_or_insert(Default::default());
|
||||||
.get_or_insert(Default::default())
|
summary.text.extend(lines.next());
|
||||||
.text
|
|
||||||
.push_str(&text);
|
|
||||||
cx.emit(ConversationEvent::SummaryChanged);
|
cx.emit(ConversationEvent::SummaryChanged);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Stop if the LLM generated multiple lines.
|
||||||
|
if lines.next().is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
|
|
203
crates/assistant/src/conversation_store.rs
Normal file
203
crates/assistant/src/conversation_store.rs
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use collections::HashMap;
|
||||||
|
use fs::Fs;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use fuzzy::StringMatchCandidate;
|
||||||
|
use gpui::{AppContext, Model, ModelContext, Task};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
use ui::Context;
|
||||||
|
use util::{paths::CONVERSATIONS_DIR, ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct SavedMessage {
|
||||||
|
pub id: MessageId,
|
||||||
|
pub start: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct SavedConversation {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub zed: String,
|
||||||
|
pub version: String,
|
||||||
|
pub text: String,
|
||||||
|
pub messages: Vec<SavedMessage>,
|
||||||
|
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||||
|
pub summary: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SavedConversation {
|
||||||
|
pub const VERSION: &'static str = "0.2.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct SavedConversationV0_1_0 {
|
||||||
|
id: Option<String>,
|
||||||
|
zed: String,
|
||||||
|
version: String,
|
||||||
|
text: String,
|
||||||
|
messages: Vec<SavedMessage>,
|
||||||
|
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||||
|
summary: String,
|
||||||
|
api_url: Option<String>,
|
||||||
|
model: OpenAiModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SavedConversationMetadata {
|
||||||
|
pub title: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub mtime: chrono::DateTime<chrono::Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ConversationStore {
|
||||||
|
conversations_metadata: Vec<SavedConversationMetadata>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
_watch_updates: Task<Option<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConversationStore {
|
||||||
|
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||||
|
let (mut events, _) = fs
|
||||||
|
.watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||||
|
conversations_metadata: Vec::new(),
|
||||||
|
fs,
|
||||||
|
_watch_updates: cx.spawn(|this, mut cx| {
|
||||||
|
async move {
|
||||||
|
while events.next().await.is_some() {
|
||||||
|
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
.log_err()
|
||||||
|
}),
|
||||||
|
})?;
|
||||||
|
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
Ok(this)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedConversation>> {
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
let saved_conversation = fs.load(&path).await?;
|
||||||
|
let saved_conversation_json =
|
||||||
|
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||||
|
match saved_conversation_json
|
||||||
|
.get("version")
|
||||||
|
.ok_or_else(|| anyhow!("version not found"))?
|
||||||
|
{
|
||||||
|
serde_json::Value::String(version) => match version.as_str() {
|
||||||
|
SavedConversation::VERSION => Ok(serde_json::from_value::<SavedConversation>(
|
||||||
|
saved_conversation_json,
|
||||||
|
)?),
|
||||||
|
"0.1.0" => {
|
||||||
|
let saved_conversation = serde_json::from_value::<SavedConversationV0_1_0>(
|
||||||
|
saved_conversation_json,
|
||||||
|
)?;
|
||||||
|
Ok(SavedConversation {
|
||||||
|
id: saved_conversation.id,
|
||||||
|
zed: saved_conversation.zed,
|
||||||
|
version: saved_conversation.version,
|
||||||
|
text: saved_conversation.text,
|
||||||
|
messages: saved_conversation.messages,
|
||||||
|
message_metadata: saved_conversation.message_metadata,
|
||||||
|
summary: saved_conversation.summary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => Err(anyhow!(
|
||||||
|
"unrecognized saved conversation version: {}",
|
||||||
|
version
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
_ => Err(anyhow!("version not found on saved conversation")),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedConversationMetadata>> {
|
||||||
|
let metadata = self.conversations_metadata.clone();
|
||||||
|
let executor = cx.background_executor().clone();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
if query.is_empty() {
|
||||||
|
metadata
|
||||||
|
} else {
|
||||||
|
let candidates = metadata
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let matches = fuzzy::match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
executor,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
matches
|
||||||
|
.into_iter()
|
||||||
|
.map(|mat| metadata[mat.candidate_id].clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||||
|
|
||||||
|
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||||
|
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||||
|
while let Some(path) = paths.next().await {
|
||||||
|
let path = path?;
|
||||||
|
if path.extension() != Some(OsStr::new("json")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pattern = r" - \d+.zed.json$";
|
||||||
|
let re = Regex::new(pattern).unwrap();
|
||||||
|
|
||||||
|
let metadata = fs.metadata(&path).await?;
|
||||||
|
if let Some((file_name, metadata)) = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.zip(metadata)
|
||||||
|
{
|
||||||
|
// This is used to filter out conversations saved by the new assistant.
|
||||||
|
if !re.is_match(file_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||||
|
conversations.push(SavedConversationMetadata {
|
||||||
|
title: title.to_string(),
|
||||||
|
path,
|
||||||
|
mtime: metadata.mtime.into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.conversations_metadata = conversations;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,126 +0,0 @@
|
||||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use collections::HashMap;
|
|
||||||
use fs::Fs;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{
|
|
||||||
cmp::Reverse,
|
|
||||||
ffi::OsStr,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use util::paths::CONVERSATIONS_DIR;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct SavedMessage {
|
|
||||||
pub id: MessageId,
|
|
||||||
pub start: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct SavedConversation {
|
|
||||||
pub id: Option<String>,
|
|
||||||
pub zed: String,
|
|
||||||
pub version: String,
|
|
||||||
pub text: String,
|
|
||||||
pub messages: Vec<SavedMessage>,
|
|
||||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
|
||||||
pub summary: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SavedConversation {
|
|
||||||
pub const VERSION: &'static str = "0.2.0";
|
|
||||||
|
|
||||||
pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
|
|
||||||
let saved_conversation = fs.load(path).await?;
|
|
||||||
let saved_conversation_json =
|
|
||||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
|
||||||
match saved_conversation_json
|
|
||||||
.get("version")
|
|
||||||
.ok_or_else(|| anyhow!("version not found"))?
|
|
||||||
{
|
|
||||||
serde_json::Value::String(version) => match version.as_str() {
|
|
||||||
Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
|
|
||||||
"0.1.0" => {
|
|
||||||
let saved_conversation =
|
|
||||||
serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
|
|
||||||
Ok(Self {
|
|
||||||
id: saved_conversation.id,
|
|
||||||
zed: saved_conversation.zed,
|
|
||||||
version: saved_conversation.version,
|
|
||||||
text: saved_conversation.text,
|
|
||||||
messages: saved_conversation.messages,
|
|
||||||
message_metadata: saved_conversation.message_metadata,
|
|
||||||
summary: saved_conversation.summary,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(anyhow!(
|
|
||||||
"unrecognized saved conversation version: {}",
|
|
||||||
version
|
|
||||||
)),
|
|
||||||
},
|
|
||||||
_ => Err(anyhow!("version not found on saved conversation")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
struct SavedConversationV0_1_0 {
|
|
||||||
id: Option<String>,
|
|
||||||
zed: String,
|
|
||||||
version: String,
|
|
||||||
text: String,
|
|
||||||
messages: Vec<SavedMessage>,
|
|
||||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
|
||||||
summary: String,
|
|
||||||
api_url: Option<String>,
|
|
||||||
model: OpenAiModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SavedConversationMetadata {
|
|
||||||
pub title: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub mtime: chrono::DateTime<chrono::Local>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SavedConversationMetadata {
|
|
||||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
|
||||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
|
||||||
|
|
||||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
|
||||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
|
||||||
while let Some(path) = paths.next().await {
|
|
||||||
let path = path?;
|
|
||||||
if path.extension() != Some(OsStr::new("json")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pattern = r" - \d+.zed.json$";
|
|
||||||
let re = Regex::new(pattern).unwrap();
|
|
||||||
|
|
||||||
let metadata = fs.metadata(&path).await?;
|
|
||||||
if let Some((file_name, metadata)) = path
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.zip(metadata)
|
|
||||||
{
|
|
||||||
// This is used to filter out conversations saved by the new assistant.
|
|
||||||
if !re.is_match(file_name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let title = re.replace(file_name, "");
|
|
||||||
conversations.push(Self {
|
|
||||||
title: title.into_owned(),
|
|
||||||
path,
|
|
||||||
mtime: metadata.mtime.into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
|
||||||
|
|
||||||
Ok(conversations)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue