diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 41087326d2..71c34da201 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -197,7 +197,7 @@ } }, { - "context": "ContextEditor > Editor", + "context": "AssistantEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", "cmd->": "assistant::QuoteSelection" diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index d61dec9f72..0f7ca509d4 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -19,8 +19,8 @@ use gpui::{ use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry}; use settings::SettingsStore; -use std::{cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; -use util::{post_inc, ResultExt, TryFutureExt}; +use std::{borrow::Cow, cell::RefCell, io, rc::Rc, sync::Arc, time::Duration}; +use util::{post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, item::Item, @@ -69,7 +69,7 @@ pub struct AssistantPanel { has_read_credentials: bool, languages: Arc, fs: Arc, - _subscriptions: Vec, + subscriptions: Vec, } impl AssistantPanel { @@ -145,11 +145,11 @@ impl AssistantPanel { fs: workspace.app_state().fs.clone(), width: None, height: None, - _subscriptions: Default::default(), + subscriptions: Default::default(), }; let mut old_dock_position = this.position(cx); - this._subscriptions = vec![ + this.subscriptions = vec![ cx.observe(&this.pane, |_, _, cx| cx.notify()), cx.subscribe(&this.pane, Self::handle_pane_event), cx.observe_global::(move |this, cx| { @@ -186,11 +186,24 @@ impl AssistantPanel { let focus = self.has_focus(cx); let editor = cx .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx)); + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_assistant_editor_event)); self.pane.update(cx, |pane, cx| { pane.add_item(Box::new(editor), true, focus, None, cx) }); } + fn handle_assistant_editor_event( + &mut self, + _: ViewHandle, + event: &AssistantEditorEvent, + cx: &mut ViewContext, + ) { + match event { + AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()), + } + } + fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { if let Some(api_key) = self .api_key_editor @@ -396,12 +409,15 @@ impl Panel for AssistantPanel { enum AssistantEvent { MessagesEdited { ids: Vec }, + SummaryChanged, } struct Assistant { buffer: ModelHandle, messages: Vec, messages_by_id: HashMap, + summary: Option, + pending_summary: Task>, completion_count: usize, pending_completions: Vec, languages: Arc, @@ -428,6 +444,8 @@ impl Assistant { let mut this = Self { messages: Default::default(), messages_by_id: Default::default(), + summary: None, + pending_summary: Task::ready(None), completion_count: Default::default(), pending_completions: Default::default(), languages: language_registry, @@ -540,9 +558,10 @@ impl Assistant { } } - this.update(&mut cx, |this, _| { + this.update(&mut cx, |this, cx| { this.pending_completions .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); }); anyhow::Ok(()) @@ -634,6 +653,54 @@ impl Assistant { self.messages_by_id.insert(excerpt_id, message.clone()); message } + + fn summarize(&mut self, cx: &mut ModelContext) { + if self.messages.len() >= 2 && self.summary.is_none() { + let api_key = self.api_key.borrow().clone(); + if let Some(api_key) = api_key { + let messages = self + .messages + .iter() + .take(2) + .map(|message| RequestMessage { + role: message.role, + content: message.content.read(cx).text(), + }) + .chain(Some(RequestMessage { + role: Role::User, + content: "Summarize the conversation into a short title without punctuation and with as few characters as possible" + .into(), + })) + .collect(); + let request = OpenAIRequest { + model: self.model.clone(), + messages, + stream: true, + }; + + let stream = stream_completion(api_key, cx.background().clone(), request); + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + let text = choice.delta.content.unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.summary.get_or_insert(String::new()).push_str(&text); + cx.emit(AssistantEvent::SummaryChanged); + }); + } + } + + anyhow::Ok(()) + } + .log_err() + }); + } + } + } } struct PendingCompletion { @@ -641,6 +708,10 @@ struct PendingCompletion { _task: Task>, } +enum AssistantEditorEvent { + TabContentChanged, +} + struct AssistantEditor { assistant: ModelHandle, editor: ViewHandle, @@ -712,9 +783,9 @@ impl AssistantEditor { ]; Self { - _subscriptions, assistant, editor, + _subscriptions, } } @@ -767,6 +838,9 @@ impl AssistantEditor { assistant.remove_empty_messages(ids, selection_heads, cx) }); } + AssistantEvent::SummaryChanged => { + cx.emit(AssistantEditorEvent::TabContentChanged); + } } } @@ -844,15 +918,23 @@ impl AssistantEditor { assistant.set_model(new_model.into(), cx); }); } + + fn title(&self, cx: &AppContext) -> String { + self.assistant + .read(cx) + .summary + .clone() + .unwrap_or_else(|| "New Context".into()) + } } impl Entity for AssistantEditor { - type Event = (); + type Event = AssistantEditorEvent; } impl View for AssistantEditor { fn ui_name() -> &'static str { - "ContextEditor" + "AssistantEditor" } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { @@ -914,9 +996,14 @@ impl Item for AssistantEditor { &self, _: Option, style: &theme::Tab, - _: &gpui::AppContext, + cx: &gpui::AppContext, ) -> AnyElement { - Label::new("New Context", style.label.clone()).into_any() + let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN); + Label::new(title, style.label.clone()).into_any() + } + + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + Some(self.title(cx).into()) } } @@ -964,9 +1051,19 @@ async fn stream_completion( while let Some(line) = lines.next().await { if let Some(event) = parse_line(line).transpose() { + let done = event.as_ref().map_or(false, |event| { + event + .choices + .last() + .map_or(false, |choice| choice.finish_reason.is_some()) + }); if tx.unbounded_send(event).is_err() { break; } + + if done { + break; + } } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1fa8c15f9b..ca5e4e981c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1151,7 +1151,8 @@ impl Pane { let theme = theme::current(cx).clone(); let mut tooltip_theme = theme.tooltip.clone(); tooltip_theme.max_text_width = None; - let tab_tooltip_text = item.tab_tooltip_text(cx).map(|a| a.to_string()); + let tab_tooltip_text = + item.tab_tooltip_text(cx).map(|text| text.into_owned()); move |mouse_state, cx| { let tab_style =