Allow to regenerate a summary of the assistant context (#14964)

Both manual and LLM-through ways are supported:


https://github.com/user-attachments/assets/afb0d2b3-9a9b-4f78-a909-1e663e686323


Release Notes:

- Improved assistant panel summarization usability
This commit is contained in:
Kirill Bulatov 2024-07-23 17:51:49 +03:00 committed by GitHub
parent a0d687c24a
commit a5cb66f0e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 204 additions and 100 deletions

1
Cargo.lock generated
View file

@ -375,7 +375,6 @@ dependencies = [
"assets", "assets",
"assistant_slash_command", "assistant_slash_command",
"async-watch", "async-watch",
"breadcrumbs",
"cargo_toml", "cargo_toml",
"chrono", "chrono",
"client", "client",

View file

@ -26,7 +26,6 @@ anyhow.workspace = true
assets.workspace = true assets.workspace = true
assistant_slash_command.workspace = true assistant_slash_command.workspace = true
async-watch.workspace = true async-watch.workspace = true
breadcrumbs.workspace = true
cargo_toml.workspace = true cargo_toml.workspace = true
chrono.workspace = true chrono.workspace = true
client.workspace = true client.workspace = true

View file

@ -16,7 +16,6 @@ use crate::{
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection}; use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use breadcrumbs::Breadcrumbs;
use client::proto; use client::proto;
use collections::{BTreeSet, HashMap, HashSet}; use collections::{BTreeSet, HashMap, HashSet};
use completion::CompletionProvider; use completion::CompletionProvider;
@ -50,6 +49,7 @@ use project::{Project, ProjectLspAdapterDelegate};
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::Settings; use settings::Settings;
use std::{ use std::{
borrow::Cow,
cmp::{self, Ordering}, cmp::{self, Ordering},
fmt::Write, fmt::Write,
ops::Range, ops::Range,
@ -58,7 +58,6 @@ use std::{
time::Duration, time::Duration,
}; };
use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
use theme::ThemeSettings;
use ui::{ use ui::{
prelude::*, prelude::*,
utils::{format_distance_from_now, DateTimeType}, utils::{format_distance_from_now, DateTimeType},
@ -68,7 +67,7 @@ use ui::{
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
item::{self, BreadcrumbText, FollowableItem, Item, ItemHandle}, item::{self, FollowableItem, Item, ItemHandle},
notifications::NotifyTaskExt, notifications::NotifyTaskExt,
pane::{self, SaveIntent}, pane::{self, SaveIntent},
searchable::{SearchEvent, SearchableItem}, searchable::{SearchEvent, SearchableItem},
@ -113,6 +112,7 @@ pub struct AssistantPanel {
subscriptions: Vec<Subscription>, subscriptions: Vec<Subscription>,
authentication_prompt: Option<AnyView>, authentication_prompt: Option<AnyView>,
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>, model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
model_summary_editor: View<Editor>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -300,6 +300,14 @@ impl AssistantPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let model_selector_menu_handle = PopoverMenuHandle::default(); 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 pane = cx.new_view(|cx| {
let mut pane = Pane::new( let mut pane = Pane::new(
workspace.weak_handle(), workspace.weak_handle(),
@ -345,13 +353,7 @@ impl AssistantPanel {
.into_any_element() .into_any_element()
}); });
pane.toolbar().update(cx, |toolbar, cx| { pane.toolbar().update(cx, |toolbar, cx| {
toolbar.add_item(cx.new_view(|_| Breadcrumbs::new()), cx); toolbar.add_item(context_editor_toolbar.clone(), cx);
toolbar.add_item(
cx.new_view(|_| {
ContextEditorToolbarItem::new(workspace, model_selector_menu_handle.clone())
}),
cx,
);
toolbar.add_item(cx.new_view(BufferSearchBar::new), cx) toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
}); });
pane pane
@ -360,6 +362,8 @@ impl AssistantPanel {
let subscriptions = vec![ let subscriptions = vec![
cx.observe(&pane, |_, _, cx| cx.notify()), cx.observe(&pane, |_, _, cx| cx.notify()),
cx.subscribe(&pane, Self::handle_pane_event), 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::<CompletionProvider>({ cx.observe_global::<CompletionProvider>({
let mut prev_settings_version = CompletionProvider::global(cx).settings_version(); let mut prev_settings_version = CompletionProvider::global(cx).settings_version();
move |this, cx| { move |this, cx| {
@ -381,6 +385,7 @@ impl AssistantPanel {
subscriptions, subscriptions,
authentication_prompt: None, authentication_prompt: None,
model_selector_menu_handle, model_selector_menu_handle,
model_summary_editor,
} }
} }
@ -390,10 +395,19 @@ impl AssistantPanel {
event: &pane::Event, event: &pane::Event,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { let update_model_summary = match event {
pane::Event::Remove => cx.emit(PanelEvent::Close), pane::Event::Remove => {
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn), cx.emit(PanelEvent::Close);
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut), false
}
pane::Event::ZoomIn => {
cx.emit(PanelEvent::ZoomIn);
false
}
pane::Event::ZoomOut => {
cx.emit(PanelEvent::ZoomOut);
false
}
pane::Event::AddItem { item } => { pane::Event::AddItem { item } => {
self.workspace self.workspace
@ -401,6 +415,7 @@ impl AssistantPanel {
item.added_to_pane(workspace, self.pane.clone(), cx) item.added_to_pane(workspace, self.pane.clone(), cx)
}) })
.ok(); .ok();
true
} }
pane::Event::ActivateItem { local } => { pane::Event::ActivateItem { local } => {
@ -412,13 +427,59 @@ impl AssistantPanel {
.ok(); .ok();
} }
cx.emit(AssistantPanelEvent::ContextEdited); cx.emit(AssistantPanelEvent::ContextEdited);
true
} }
pane::Event::RemoveItem { .. } => { pane::Event::RemoveItem { .. } => {
cx.emit(AssistantPanelEvent::ContextEdited); 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<Editor>,
event: &EditorEvent,
cx: &mut ViewContext<Self>,
) {
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<ContextEditorToolbarItem>,
_: &ContextEditorToolbarItemEvent,
cx: &mut ViewContext<Self>,
) {
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) { if self.is_authenticated(cx) {
self.authentication_prompt = None; self.authentication_prompt = None;
if let Some(editor) = self.active_context_editor(cx) { match self.active_context_editor(cx) {
Some(editor) => {
editor.update(cx, |active_context, cx| { editor.update(cx, |active_context, cx| {
active_context active_context
.context .context
.update(cx, |context, cx| context.completion_provider_changed(cx)) .update(cx, |context, cx| context.completion_provider_changed(cx))
}) });
} }
None => {
if self.active_context_editor(cx).is_none() {
self.new_context(cx); self.new_context(cx);
} }
}
cx.notify(); cx.notify();
} else if self.authentication_prompt.is_none() } else if self.authentication_prompt.is_none()
|| prev_settings_version != CompletionProvider::global(cx).settings_version() || prev_settings_version != CompletionProvider::global(cx).settings_version()
@ -637,18 +700,43 @@ impl AssistantPanel {
.push(cx.subscribe(&context_editor, Self::handle_context_editor_event)); .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
} }
self.show_updated_summary(&context_editor, cx);
cx.emit(AssistantPanelEvent::ContextEdited); cx.emit(AssistantPanelEvent::ContextEdited);
cx.notify(); cx.notify();
} }
fn show_updated_summary(
&self,
context_editor: &View<ContextEditor>,
cx: &mut ViewContext<Self>,
) {
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( fn handle_context_editor_event(
&mut self, &mut self,
_: View<ContextEditor>, context_editor: View<ContextEditor>,
event: &EditorEvent, event: &EditorEvent,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
EditorEvent::TitleChanged { .. } => cx.notify(), EditorEvent::TitleChanged => {
self.show_updated_summary(&context_editor, cx);
cx.notify()
}
EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited), EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
_ => {} _ => {}
} }
@ -1001,9 +1089,10 @@ pub struct ContextEditor {
assistant_panel: WeakView<AssistantPanel>, assistant_panel: WeakView<AssistantPanel>,
} }
impl ContextEditor { const DEFAULT_TAB_TITLE: &str = "New Context";
const MAX_TAB_TITLE_LEN: usize = 16; const MAX_TAB_TITLE_LEN: usize = 16;
impl ContextEditor {
fn for_context( fn for_context(
context: Model<Context>, context: Model<Context>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -1316,7 +1405,7 @@ impl ContextEditor {
ContextEvent::SummaryChanged => { ContextEvent::SummaryChanged => {
cx.emit(EditorEvent::TitleChanged); cx.emit(EditorEvent::TitleChanged);
self.context.update(cx, |context, cx| { 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 => { ContextEvent::StreamedCompletion => {
@ -2031,16 +2120,18 @@ impl ContextEditor {
} }
fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) { fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
self.context self.context.update(cx, |context, cx| {
.update(cx, |context, cx| context.save(None, self.fs.clone(), cx)); context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
});
} }
fn title(&self, cx: &AppContext) -> String { fn title(&self, cx: &AppContext) -> Cow<str> {
self.context self.context
.read(cx) .read(cx)
.summary() .summary()
.map(|summary| summary.text.clone()) .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<Self>) -> impl IntoElement { fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
@ -2139,14 +2230,13 @@ impl Item for ContextEditor {
type Event = editor::EditorEvent; type Event = editor::EditorEvent;
fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> { fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
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)) { fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
match event { match event {
EditorEvent::Edited { .. } => { EditorEvent::Edited { .. } => {
f(item::ItemEvent::Edit); f(item::ItemEvent::Edit);
f(item::ItemEvent::UpdateBreadcrumbs);
} }
EditorEvent::TitleChanged => { EditorEvent::TitleChanged => {
f(item::ItemEvent::UpdateTab); f(item::ItemEvent::UpdateTab);
@ -2156,48 +2246,13 @@ impl Item for ContextEditor {
} }
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> { fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
Some(self.title(cx).into()) Some(self.title(cx).to_string().into())
} }
fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> { fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone())) Some(Box::new(handle.clone()))
} }
fn breadcrumbs(
&self,
theme: &theme::Theme,
cx: &AppContext,
) -> Option<Vec<item::BreadcrumbText>> {
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>) { fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
Item::set_nav_history(editor, nav_history, cx) Item::set_nav_history(editor, nav_history, cx)
@ -2405,18 +2460,21 @@ pub struct ContextEditorToolbarItem {
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
active_context_editor: Option<WeakView<ContextEditor>>, active_context_editor: Option<WeakView<ContextEditor>>,
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>, model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
model_summary_editor: View<Editor>,
} }
impl ContextEditorToolbarItem { impl ContextEditorToolbarItem {
pub fn new( pub fn new(
workspace: &Workspace, workspace: &Workspace,
model_selector_menu_handle: PopoverMenuHandle<ContextMenu>, model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
model_summary_editor: View<Editor>,
) -> Self { ) -> Self {
Self { Self {
fs: workspace.app_state().fs.clone(), fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
active_context_editor: None, active_context_editor: None,
model_selector_menu_handle, model_selector_menu_handle,
model_summary_editor,
} }
} }
@ -2524,14 +2582,35 @@ impl ContextEditorToolbarItem {
impl Render for ContextEditorToolbarItem { impl Render for ContextEditorToolbarItem {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> 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() .gap_2()
.child(ModelSelector::new( .child(ModelSelector::new(
self.model_selector_menu_handle.clone(), self.model_selector_menu_handle.clone(),
self.fs.clone(), self.fs.clone(),
)) ))
.children(self.render_remaining_tokens(cx)) .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<ToolbarItemEvent> for ContextEditorToolbarItem {} impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
enum ContextEditorToolbarItemEvent {
RegenerateSummary,
}
impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
pub struct ContextHistory { pub struct ContextHistory {
picker: View<Picker<SavedContextPickerDelegate>>, picker: View<Picker<SavedContextPickerDelegate>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,

View file

@ -9,7 +9,7 @@ use assistant_slash_command::{
use client::{self, proto, telemetry::Telemetry}; use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use fs::Fs; use fs::{Fs, RemoveOptions};
use futures::{ use futures::{
future::{self, Shared}, future::{self, Shared},
FutureExt, StreamExt, FutureExt, StreamExt,
@ -1675,7 +1675,7 @@ impl Context {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.pending_completions this.pending_completions
.retain(|completion| completion.id != this.completion_count); .retain(|completion| completion.id != this.completion_count);
this.summarize(cx); this.summarize(false, cx);
})?; })?;
anyhow::Ok(()) anyhow::Ok(())
@ -1968,8 +1968,8 @@ impl Context {
self.message_anchors.insert(insertion_ix, new_anchor); self.message_anchors.insert(insertion_ix, new_anchor);
} }
fn summarize(&mut self, cx: &mut ModelContext<Self>) { pub(super) fn summarize(&mut self, replace_old: bool, cx: &mut ModelContext<Self>) {
if self.message_anchors.len() >= 2 && self.summary.is_none() { if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) {
if !CompletionProvider::global(cx).is_authenticated() { if !CompletionProvider::global(cx).is_authenticated() {
return; return;
} }
@ -1993,13 +1993,18 @@ impl Context {
async move { async move {
let mut messages = stream.await?; let mut messages = stream.await?;
let mut replaced = !replace_old;
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(); let mut lines = text.lines();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let version = this.version.clone(); let version = this.version.clone();
let timestamp = this.next_timestamp(); 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.text.extend(lines.next());
summary.timestamp = timestamp; summary.timestamp = timestamp;
let operation = ContextOperation::UpdateSummary { let operation = ContextOperation::UpdateSummary {
@ -2142,9 +2147,6 @@ impl Context {
if let Some(summary) = summary { if let Some(summary) = summary {
let context = this.read_with(&cx, |this, cx| this.serialize(cx))?; 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 discriminant = 1;
let mut new_path; let mut new_path;
loop { loop {
@ -2159,18 +2161,38 @@ impl Context {
break; break;
} }
} }
new_path
};
fs.create_dir(contexts_dir().as_ref()).await?; 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?; .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(()) Ok(())
}); });
} }
pub(crate) fn custom_summary(&mut self, custom_summary: String, cx: &mut ModelContext<Self>) {
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)] #[derive(Debug, Default)]