diff --git a/Cargo.lock b/Cargo.lock index 91205f214fff964cb21e4a98591da978fb2c71ab..b8c24b45942256dec0a6c8068ad1a71310db3c19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -458,13 +458,16 @@ dependencies = [ "command_palette_hooks", "editor", "feature_flags", + "futures 0.3.31", "gpui", "language_model", "language_model_selector", "proto", "settings", + "smol", "theme", "ui", + "util", "workspace", ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 963d48ba5e9ba5210ce64b284de497cea34257f8..c8bc80a9c03d1823b156727cf60b8ad16430430e 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -209,6 +209,18 @@ "alt-enter": "editor::Newline" } }, + { + "context": "AssistantPanel2", + "bindings": { + "cmd-n": "assistant2::NewThread" + } + }, + { + "context": "MessageEditor > Editor", + "bindings": { + "cmd-enter": "assistant2::Chat" + } + }, { "context": "PromptLibrary", "bindings": { diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 9dd605d55946669438d1b678a7cd18b41a432239..02cbdadb623cb6d21d337117f4750106e336de47 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -17,11 +17,14 @@ anyhow.workspace = true command_palette_hooks.workspace = true editor.workspace = true feature_flags.workspace = true +futures.workspace = true gpui.workspace = true language_model.workspace = true language_model_selector.workspace = true proto.workspace = true settings.workspace = true +smol.workspace = true theme.workspace = true ui.workspace = true +util.workspace = true workspace.workspace = true diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 6a80186525ba4acc8b02c40b99e986ceaa23c0e7..1b33e27928a609d31b8083856e51cee6878e7f8c 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -1,5 +1,6 @@ mod assistant_panel; mod message_editor; +mod thread; use command_palette_hooks::CommandPaletteFilter; use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt}; @@ -7,7 +8,10 @@ use gpui::{actions, AppContext}; pub use crate::assistant_panel::AssistantPanel; -actions!(assistant2, [ToggleFocus, NewThread, ToggleModelSelector]); +actions!( + assistant2, + [ToggleFocus, NewThread, ToggleModelSelector, Chat] +); const NAMESPACE: &str = "assistant2"; diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 890020e54af7ec91771152e84f9b5ad68e2c4de8..88a3f7317603324eb41bbd05a2b3c3883c6a0314 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1,7 +1,7 @@ use anyhow::Result; use gpui::{ prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, - FocusableView, Pixels, Task, View, ViewContext, WeakView, WindowContext, + FocusableView, Model, Pixels, Task, View, ViewContext, WeakView, WindowContext, }; use language_model::LanguageModelRegistry; use language_model_selector::LanguageModelSelector; @@ -10,6 +10,7 @@ use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::{Pane, Workspace}; use crate::message_editor::MessageEditor; +use crate::thread::Thread; use crate::{NewThread, ToggleFocus, ToggleModelSelector}; pub fn init(cx: &mut AppContext) { @@ -25,6 +26,7 @@ pub fn init(cx: &mut AppContext) { pub struct AssistantPanel { pane: View, + thread: Model, message_editor: View, } @@ -56,9 +58,12 @@ impl AssistantPanel { pane }); + let thread = cx.new_model(Thread::new); + Self { pane, - message_editor: cx.new_view(MessageEditor::new), + thread: thread.clone(), + message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)), } } } @@ -247,7 +252,24 @@ impl Render for AssistantPanel { println!("Action: New Thread"); })) .child(self.render_toolbar(cx)) - .child(v_flex().bg(cx.theme().colors().panel_background)) + .child( + v_flex() + .id("message-list") + .gap_2() + .size_full() + .p_2() + .overflow_y_scroll() + .bg(cx.theme().colors().panel_background) + .children(self.thread.read(cx).messages.iter().map(|message| { + v_flex() + .p_2() + .border_1() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .child(Label::new(message.role.to_string())) + .child(Label::new(message.text.clone())) + })), + ) .child( h_flex() .border_t_1() diff --git a/crates/assistant2/src/message_editor.rs b/crates/assistant2/src/message_editor.rs index ee25ad5da788afe7b2d9fd31e6bf58af8b73b0cb..63f8c869d43bcfb844370450dbbaa8bbcec881ae 100644 --- a/crates/assistant2/src/message_editor.rs +++ b/crates/assistant2/src/message_editor.rs @@ -1,16 +1,32 @@ use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{TextStyle, View}; +use futures::StreamExt; +use gpui::{AppContext, Model, TextStyle, View}; +use language_model::{ + LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, MessageContent, Role, StopReason, +}; use settings::Settings; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding}; +use util::ResultExt; + +use crate::thread::{self, Thread}; +use crate::Chat; + +#[derive(Debug, Clone, Copy)] +pub enum RequestKind { + Chat, +} pub struct MessageEditor { + thread: Model, editor: View, } impl MessageEditor { - pub fn new(cx: &mut ViewContext) -> Self { + pub fn new(thread: Model, cx: &mut ViewContext) -> Self { Self { + thread, editor: cx.new_view(|cx| { let mut editor = Editor::auto_height(80, cx); editor.set_placeholder_text("Ask anything…", cx); @@ -19,14 +35,122 @@ impl MessageEditor { }), } } + + fn chat(&mut self, _: &Chat, cx: &mut ViewContext) { + self.send_to_model(RequestKind::Chat, cx); + } + + fn send_to_model( + &mut self, + request_kind: RequestKind, + cx: &mut ViewContext, + ) -> Option<()> { + let provider = LanguageModelRegistry::read_global(cx).active_provider(); + if provider + .as_ref() + .map_or(false, |provider| provider.must_accept_terms(cx)) + { + cx.notify(); + return None; + } + + let model_registry = LanguageModelRegistry::read_global(cx); + let model = model_registry.active_model()?; + + let request = self.build_completion_request(request_kind, cx); + + let user_message = self.editor.read(cx).text(cx); + self.thread.update(cx, |thread, _cx| { + thread.messages.push(thread::Message { + role: Role::User, + text: user_message, + }); + }); + + self.editor.update(cx, |editor, cx| { + editor.clear(cx); + }); + + let task = cx.spawn(|this, mut cx| async move { + let stream = model.stream_completion(request, &cx); + let stream_completion = async { + let mut events = stream.await?; + let mut stop_reason = StopReason::EndTurn; + + let mut text = String::new(); + + while let Some(event) = events.next().await { + let event = event?; + match event { + LanguageModelCompletionEvent::StartMessage { .. } => {} + LanguageModelCompletionEvent::Stop(reason) => { + stop_reason = reason; + } + LanguageModelCompletionEvent::Text(chunk) => { + text.push_str(&chunk); + } + LanguageModelCompletionEvent::ToolUse(_tool_use) => {} + } + + smol::future::yield_now().await; + } + + anyhow::Ok((stop_reason, text)) + }; + + let result = stream_completion.await; + + this.update(&mut cx, |this, cx| { + if let Some((_stop_reason, text)) = result.log_err() { + this.thread.update(cx, |thread, _cx| { + thread.messages.push(thread::Message { + role: Role::Assistant, + text, + }); + }); + } + }) + .ok(); + }); + + self.thread.update(cx, |thread, _cx| { + thread.pending_completion_tasks.push(task); + }); + + None + } + + fn build_completion_request( + &self, + _request_kind: RequestKind, + cx: &AppContext, + ) -> LanguageModelRequest { + let text = self.editor.read(cx).text(cx); + + let request = LanguageModelRequest { + messages: vec![LanguageModelRequestMessage { + role: Role::User, + content: vec![MessageContent::Text(text)], + cache: false, + }], + tools: Vec::new(), + stop: Vec::new(), + temperature: None, + }; + + request + } } impl Render for MessageEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let font_size = TextSize::Default.rems(cx); let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; + let focus_handle = self.editor.focus_handle(cx); v_flex() + .key_context("MessageEditor") + .on_action(cx.listener(Self::chat)) .size_full() .gap_2() .p_2() @@ -69,7 +193,19 @@ impl Render for MessageEditor { .gap_2() .child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled)) .child(Label::new("or")) - .child(Button::new("chat", "Chat").style(ButtonStyle::Filled)), + .child( + ButtonLike::new("chat") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .child(Label::new("Chat")) + .children( + KeyBinding::for_action_in(&Chat, &focus_handle, cx) + .map(|binding| binding.into_any_element()), + ) + .on_click(move |_event, cx| { + focus_handle.dispatch_action(&Chat, cx); + }), + ), ), ) } diff --git a/crates/assistant2/src/thread.rs b/crates/assistant2/src/thread.rs new file mode 100644 index 0000000000000000000000000000000000000000..1553eaabb6f264dc851f30e6c79e1a77874152c8 --- /dev/null +++ b/crates/assistant2/src/thread.rs @@ -0,0 +1,23 @@ +use gpui::{ModelContext, Task}; +use language_model::Role; + +/// A message in a [`Thread`]. +pub struct Message { + pub role: Role, + pub text: String, +} + +/// A thread of conversation with the LLM. +pub struct Thread { + pub messages: Vec, + pub pending_completion_tasks: Vec>, +} + +impl Thread { + pub fn new(_cx: &mut ModelContext) -> Self { + Self { + messages: Vec::new(), + pending_completion_tasks: Vec::new(), + } + } +}