diff --git a/Cargo.lock b/Cargo.lock index ad1b4405f2..8e7326b5b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,7 +375,6 @@ dependencies = [ "assets", "assistant_slash_command", "async-watch", - "breadcrumbs", "cargo_toml", "chrono", "client", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 201e16bd57..4c371377f4 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -26,7 +26,6 @@ anyhow.workspace = true assets.workspace = true assistant_slash_command.workspace = true async-watch.workspace = true -breadcrumbs.workspace = true cargo_toml.workspace = true chrono.workspace = true client.workspace = true diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index e02c26837a..8524da4066 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -16,7 +16,6 @@ use crate::{ }; use anyhow::{anyhow, Result}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; -use breadcrumbs::Breadcrumbs; use client::proto; use collections::{BTreeSet, HashMap, HashSet}; use completion::CompletionProvider; @@ -50,6 +49,7 @@ use project::{Project, ProjectLspAdapterDelegate}; use search::{buffer_search::DivRegistrar, BufferSearchBar}; use settings::Settings; use std::{ + borrow::Cow, cmp::{self, Ordering}, fmt::Write, ops::Range, @@ -58,7 +58,6 @@ use std::{ time::Duration, }; use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; -use theme::ThemeSettings; use ui::{ prelude::*, utils::{format_distance_from_now, DateTimeType}, @@ -68,7 +67,7 @@ use ui::{ use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle}, + item::{self, FollowableItem, Item, ItemHandle}, notifications::NotifyTaskExt, pane::{self, SaveIntent}, searchable::{SearchEvent, SearchableItem}, @@ -113,6 +112,7 @@ pub struct AssistantPanel { subscriptions: Vec, authentication_prompt: Option, model_selector_menu_handle: PopoverMenuHandle, + model_summary_editor: View, } #[derive(Clone)] @@ -300,6 +300,14 @@ impl AssistantPanel { cx: &mut ViewContext, ) -> Self { let model_selector_menu_handle = PopoverMenuHandle::default(); + let model_summary_editor = cx.new_view(|cx| Editor::single_line(cx)); + let context_editor_toolbar = cx.new_view(|_| { + ContextEditorToolbarItem::new( + workspace, + model_selector_menu_handle.clone(), + model_summary_editor.clone(), + ) + }); let pane = cx.new_view(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -345,13 +353,7 @@ impl AssistantPanel { .into_any_element() }); pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(cx.new_view(|_| Breadcrumbs::new()), cx); - toolbar.add_item( - cx.new_view(|_| { - ContextEditorToolbarItem::new(workspace, model_selector_menu_handle.clone()) - }), - cx, - ); + toolbar.add_item(context_editor_toolbar.clone(), cx); toolbar.add_item(cx.new_view(BufferSearchBar::new), cx) }); pane @@ -360,6 +362,8 @@ impl AssistantPanel { let subscriptions = vec![ cx.observe(&pane, |_, _, cx| cx.notify()), cx.subscribe(&pane, Self::handle_pane_event), + cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event), + cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event), cx.observe_global::({ let mut prev_settings_version = CompletionProvider::global(cx).settings_version(); move |this, cx| { @@ -381,6 +385,7 @@ impl AssistantPanel { subscriptions, authentication_prompt: None, model_selector_menu_handle, + model_summary_editor, } } @@ -390,10 +395,19 @@ impl AssistantPanel { event: &pane::Event, cx: &mut ViewContext, ) { - match event { - pane::Event::Remove => cx.emit(PanelEvent::Close), - pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), - pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), + let update_model_summary = match event { + pane::Event::Remove => { + cx.emit(PanelEvent::Close); + false + } + pane::Event::ZoomIn => { + cx.emit(PanelEvent::ZoomIn); + false + } + pane::Event::ZoomOut => { + cx.emit(PanelEvent::ZoomOut); + false + } pane::Event::AddItem { item } => { self.workspace @@ -401,6 +415,7 @@ impl AssistantPanel { item.added_to_pane(workspace, self.pane.clone(), cx) }) .ok(); + true } pane::Event::ActivateItem { local } => { @@ -412,13 +427,59 @@ impl AssistantPanel { .ok(); } cx.emit(AssistantPanelEvent::ContextEdited); + true } pane::Event::RemoveItem { .. } => { cx.emit(AssistantPanelEvent::ContextEdited); + true } - _ => {} + _ => false, + }; + + if update_model_summary { + if let Some(editor) = self.active_context_editor(cx) { + self.show_updated_summary(&editor, cx) + } + } + } + + fn handle_summary_editor_event( + &mut self, + model_summary_editor: View, + event: &EditorEvent, + cx: &mut ViewContext, + ) { + if matches!(event, EditorEvent::Edited { .. }) { + if let Some(context_editor) = self.active_context_editor(cx) { + let new_summary = model_summary_editor.read(cx).text(cx); + context_editor.update(cx, |context_editor, cx| { + context_editor.context.update(cx, |context, cx| { + if context.summary().is_none() + && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty()) + { + return; + } + context.custom_summary(new_summary, cx) + }); + }); + } + } + } + + fn handle_toolbar_event( + &mut self, + _: View, + _: &ContextEditorToolbarItemEvent, + cx: &mut ViewContext, + ) { + if let Some(context_editor) = self.active_context_editor(cx) { + context_editor.update(cx, |context_editor, cx| { + context_editor.context.update(cx, |context, cx| { + context.summarize(true, cx); + }) + }) } } @@ -430,17 +491,19 @@ impl AssistantPanel { if self.is_authenticated(cx) { self.authentication_prompt = None; - if let Some(editor) = self.active_context_editor(cx) { - editor.update(cx, |active_context, cx| { - active_context - .context - .update(cx, |context, cx| context.completion_provider_changed(cx)) - }) + match self.active_context_editor(cx) { + Some(editor) => { + editor.update(cx, |active_context, cx| { + active_context + .context + .update(cx, |context, cx| context.completion_provider_changed(cx)) + }); + } + None => { + self.new_context(cx); + } } - if self.active_context_editor(cx).is_none() { - self.new_context(cx); - } cx.notify(); } else if self.authentication_prompt.is_none() || prev_settings_version != CompletionProvider::global(cx).settings_version() @@ -637,18 +700,43 @@ impl AssistantPanel { .push(cx.subscribe(&context_editor, Self::handle_context_editor_event)); } + self.show_updated_summary(&context_editor, cx); + cx.emit(AssistantPanelEvent::ContextEdited); cx.notify(); } + fn show_updated_summary( + &self, + context_editor: &View, + cx: &mut ViewContext, + ) { + context_editor.update(cx, |context_editor, cx| { + let new_summary = context_editor + .context + .read(cx) + .summary() + .map(|s| s.text.clone()) + .unwrap_or_else(|| context_editor.title(cx).to_string()); + self.model_summary_editor.update(cx, |summary_editor, cx| { + if summary_editor.text(cx) != new_summary { + summary_editor.set_text(new_summary, cx); + } + }); + }); + } + fn handle_context_editor_event( &mut self, - _: View, + context_editor: View, event: &EditorEvent, cx: &mut ViewContext, ) { match event { - EditorEvent::TitleChanged { .. } => cx.notify(), + EditorEvent::TitleChanged => { + self.show_updated_summary(&context_editor, cx); + cx.notify() + } EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited), _ => {} } @@ -1001,9 +1089,10 @@ pub struct ContextEditor { assistant_panel: WeakView, } -impl ContextEditor { - const MAX_TAB_TITLE_LEN: usize = 16; +const DEFAULT_TAB_TITLE: &str = "New Context"; +const MAX_TAB_TITLE_LEN: usize = 16; +impl ContextEditor { fn for_context( context: Model, fs: Arc, @@ -1316,7 +1405,7 @@ impl ContextEditor { ContextEvent::SummaryChanged => { cx.emit(EditorEvent::TitleChanged); self.context.update(cx, |context, cx| { - context.save(None, self.fs.clone(), cx); + context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); }); } ContextEvent::StreamedCompletion => { @@ -2031,16 +2120,18 @@ impl ContextEditor { } fn save(&mut self, _: &Save, cx: &mut ViewContext) { - self.context - .update(cx, |context, cx| context.save(None, self.fs.clone(), cx)); + self.context.update(cx, |context, cx| { + context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx) + }); } - fn title(&self, cx: &AppContext) -> String { + fn title(&self, cx: &AppContext) -> Cow { self.context .read(cx) .summary() .map(|summary| summary.text.clone()) - .unwrap_or_else(|| "New Context".into()) + .map(Cow::Owned) + .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE)) } fn render_send_button(&self, cx: &mut ViewContext) -> impl IntoElement { @@ -2139,14 +2230,13 @@ impl Item for ContextEditor { type Event = editor::EditorEvent; fn tab_content_text(&self, cx: &WindowContext) -> Option { - Some(util::truncate_and_trailoff(&self.title(cx), Self::MAX_TAB_TITLE_LEN).into()) + Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into()) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) { match event { EditorEvent::Edited { .. } => { f(item::ItemEvent::Edit); - f(item::ItemEvent::UpdateBreadcrumbs); } EditorEvent::TitleChanged => { f(item::ItemEvent::UpdateTab); @@ -2156,48 +2246,13 @@ impl Item for ContextEditor { } fn tab_tooltip_text(&self, cx: &AppContext) -> Option { - Some(self.title(cx).into()) + Some(self.title(cx).to_string().into()) } fn as_searchable(&self, handle: &View) -> Option> { Some(Box::new(handle.clone())) } - fn breadcrumbs( - &self, - theme: &theme::Theme, - cx: &AppContext, - ) -> Option> { - let editor = self.editor.read(cx); - let cursor = editor.selections.newest_anchor().head(); - let multibuffer = &editor.buffer().read(cx); - let (_, symbols) = multibuffer.symbols_containing(cursor, Some(&theme.syntax()), cx)?; - - let settings = ThemeSettings::get_global(cx); - - let mut breadcrumbs = Vec::new(); - - let title = self.title(cx); - if title.chars().count() > Self::MAX_TAB_TITLE_LEN { - breadcrumbs.push(BreadcrumbText { - text: title, - highlights: None, - font: Some(settings.buffer_font.clone()), - }); - } - - breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText { - text: symbol.text, - highlights: Some(symbol.highlight_ranges), - font: Some(settings.buffer_font.clone()), - })); - Some(breadcrumbs) - } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| { Item::set_nav_history(editor, nav_history, cx) @@ -2405,18 +2460,21 @@ pub struct ContextEditorToolbarItem { workspace: WeakView, active_context_editor: Option>, model_selector_menu_handle: PopoverMenuHandle, + model_summary_editor: View, } impl ContextEditorToolbarItem { pub fn new( workspace: &Workspace, model_selector_menu_handle: PopoverMenuHandle, + model_summary_editor: View, ) -> Self { Self { fs: workspace.app_state().fs.clone(), workspace: workspace.weak_handle(), active_context_editor: None, model_selector_menu_handle, + model_summary_editor, } } @@ -2524,14 +2582,35 @@ impl ContextEditorToolbarItem { impl Render for ContextEditorToolbarItem { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - h_flex() + let left_side = h_flex() + .gap_2() + .flex_1() + .min_w(rems(DEFAULT_TAB_TITLE.len() as f32)) + .when(self.active_context_editor.is_some(), |left_side| { + left_side + .child( + IconButton::new("regenerate-context", IconName::ArrowCircle) + .tooltip(|cx| Tooltip::text("Regenerate Summary", cx)) + .on_click(cx.listener(move |_, _, cx| { + cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary) + })), + ) + .child(self.model_summary_editor.clone()) + }); + let right_side = h_flex() .gap_2() .child(ModelSelector::new( self.model_selector_menu_handle.clone(), self.fs.clone(), )) .children(self.render_remaining_tokens(cx)) - .child(self.render_inject_context_menu(cx)) + .child(self.render_inject_context_menu(cx)); + + h_flex() + .size_full() + .justify_between() + .child(left_side) + .child(right_side) } } @@ -2559,6 +2638,11 @@ impl ToolbarItemView for ContextEditorToolbarItem { impl EventEmitter for ContextEditorToolbarItem {} +enum ContextEditorToolbarItemEvent { + RegenerateSummary, +} +impl EventEmitter for ContextEditorToolbarItem {} + pub struct ContextHistory { picker: View>, _subscriptions: Vec, diff --git a/crates/assistant/src/context.rs b/crates/assistant/src/context.rs index f75b693bbd..22511ab896 100644 --- a/crates/assistant/src/context.rs +++ b/crates/assistant/src/context.rs @@ -9,7 +9,7 @@ use assistant_slash_command::{ use client::{self, proto, telemetry::Telemetry}; use clock::ReplicaId; use collections::{HashMap, HashSet}; -use fs::Fs; +use fs::{Fs, RemoveOptions}; use futures::{ future::{self, Shared}, FutureExt, StreamExt, @@ -1675,7 +1675,7 @@ impl Context { this.update(&mut cx, |this, cx| { this.pending_completions .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); + this.summarize(false, cx); })?; anyhow::Ok(()) @@ -1968,8 +1968,8 @@ impl Context { self.message_anchors.insert(insertion_ix, new_anchor); } - fn summarize(&mut self, cx: &mut ModelContext) { - if self.message_anchors.len() >= 2 && self.summary.is_none() { + pub(super) fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext) { + if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) { if !CompletionProvider::global(cx).is_authenticated() { return; } @@ -1993,13 +1993,18 @@ impl Context { async move { let mut messages = stream.await?; + let mut replaced = !replace_old; while let Some(message) = messages.next().await { let text = message?; let mut lines = text.lines(); this.update(&mut cx, |this, cx| { let version = this.version.clone(); let timestamp = this.next_timestamp(); - let summary = this.summary.get_or_insert(Default::default()); + let summary = this.summary.get_or_insert(ContextSummary::default()); + if !replaced && replace_old { + summary.text.clear(); + replaced = true; + } summary.text.extend(lines.next()); summary.timestamp = timestamp; let operation = ContextOperation::UpdateSummary { @@ -2142,35 +2147,52 @@ impl Context { if let Some(summary) = summary { let context = this.read_with(&cx, |this, cx| this.serialize(cx))?; - let path = if let Some(old_path) = old_path { - old_path - } else { - let mut discriminant = 1; - let mut new_path; - loop { - new_path = contexts_dir().join(&format!( - "{} - {}.zed.json", - summary.trim(), - discriminant - )); - if fs.is_file(&new_path).await { - discriminant += 1; - } else { - break; - } + let mut discriminant = 1; + let mut new_path; + loop { + new_path = contexts_dir().join(&format!( + "{} - {}.zed.json", + summary.trim(), + discriminant + )); + if fs.is_file(&new_path).await { + discriminant += 1; + } else { + break; } - new_path - }; + } fs.create_dir(contexts_dir().as_ref()).await?; - fs.atomic_write(path.clone(), serde_json::to_string(&context).unwrap()) + fs.atomic_write(new_path.clone(), serde_json::to_string(&context).unwrap()) .await?; - this.update(&mut cx, |this, _| this.path = Some(path))?; + if let Some(old_path) = old_path { + if new_path != old_path { + fs.remove_file( + &old_path, + RemoveOptions { + recursive: false, + ignore_if_not_exists: true, + }, + ) + .await?; + } + } + + this.update(&mut cx, |this, _| this.path = Some(new_path))?; } Ok(()) }); } + + pub(crate) fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext) { + let timestamp = self.next_timestamp(); + let summary = self.summary.get_or_insert(ContextSummary::default()); + summary.timestamp = timestamp; + summary.done = true; + summary.text = custom_summary; + cx.emit(ContextEvent::SummaryChanged); + } } #[derive(Debug, Default)]