Detailed changes
@@ -8,11 +8,6 @@ license = "GPL-3.0-or-later"
[lib]
path = "src/assistant2.rs"
-[[example]]
-name = "assistant_example"
-path = "examples/assistant_example.rs"
-crate-type = ["bin"]
-
[dependencies]
anyhow.workspace = true
assistant_tooling.workspace = true
@@ -1,127 +0,0 @@
-use anyhow::Context as _;
-use assets::Assets;
-use assistant2::{tools::ProjectIndexTool, AssistantPanel};
-use assistant_tooling::ToolRegistry;
-use client::Client;
-use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
-use language::LanguageRegistry;
-use project::Project;
-use semantic_index::{OpenAiEmbeddingModel, OpenAiEmbeddingProvider, SemanticIndex};
-use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
-use std::{
- path::{Path, PathBuf},
- sync::Arc,
-};
-use theme::LoadThemes;
-use ui::{div, prelude::*, Render};
-use util::{http::HttpClientWithUrl, ResultExt as _};
-
-actions!(example, [Quit]);
-
-fn main() {
- let args: Vec<String> = std::env::args().collect();
-
- env_logger::init();
- App::new().with_assets(Assets).run(|cx| {
- cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
- cx.on_action(|_: &Quit, cx: &mut AppContext| {
- cx.quit();
- });
-
- if args.len() < 2 {
- eprintln!(
- "Usage: cargo run --example assistant_example -p assistant2 -- <project_path>"
- );
- cx.quit();
- return;
- }
-
- settings::init(cx);
- language::init(cx);
- Project::init_settings(cx);
- editor::init(cx);
- theme::init(LoadThemes::JustBase, cx);
- Assets.load_fonts(cx).unwrap();
- KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
- client::init_settings(cx);
- release_channel::init("0.130.0", cx);
-
- let client = Client::production(cx);
- {
- let client = client.clone();
- cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
- .detach_and_log_err(cx);
- }
- assistant2::init(client.clone(), cx);
-
- let language_registry = Arc::new(LanguageRegistry::new(
- Task::ready(()),
- cx.background_executor().clone(),
- ));
- let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
- languages::init(language_registry.clone(), node_runtime, cx);
-
- let http = Arc::new(HttpClientWithUrl::new("http://localhost:11434"));
-
- let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set");
- let embedding_provider = OpenAiEmbeddingProvider::new(
- http.clone(),
- OpenAiEmbeddingModel::TextEmbedding3Small,
- open_ai::OPEN_AI_API_URL.to_string(),
- api_key,
- );
-
- cx.spawn(|mut cx| async move {
- let mut semantic_index = SemanticIndex::new(
- PathBuf::from("/tmp/semantic-index-db.mdb"),
- Arc::new(embedding_provider),
- &mut cx,
- )
- .await?;
-
- let project_path = Path::new(&args[1]);
- let project = Project::example([project_path], &mut cx).await;
-
- cx.update(|cx| {
- let fs = project.read(cx).fs().clone();
-
- let project_index = semantic_index.project_index(project.clone(), cx);
-
- cx.open_window(WindowOptions::default(), |cx| {
- let mut tool_registry = ToolRegistry::new();
- tool_registry
- .register(ProjectIndexTool::new(project_index.clone(), fs.clone()), cx)
- .context("failed to register ProjectIndexTool")
- .log_err();
-
- cx.new_view(|cx| Example::new(language_registry, Arc::new(tool_registry), cx))
- });
- cx.activate(true);
- })
- })
- .detach_and_log_err(cx);
- })
-}
-
-struct Example {
- assistant_panel: View<AssistantPanel>,
-}
-
-impl Example {
- fn new(
- language_registry: Arc<LanguageRegistry>,
- tool_registry: Arc<ToolRegistry>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- Self {
- assistant_panel: cx
- .new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
- }
- }
-}
-
-impl Render for Example {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
- div().size_full().child(self.assistant_panel.clone())
- }
-}
@@ -4,15 +4,17 @@ use anyhow::{Context as _, Result};
use assets::Assets;
use assistant2::AssistantPanel;
use assistant_tooling::{LanguageModelTool, ToolRegistry};
-use client::Client;
-use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Task, View, WindowOptions};
+use client::{Client, UserStore};
+use fs::Fs;
+use futures::StreamExt as _;
+use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
use language::LanguageRegistry;
use project::Project;
use rand::Rng;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
-use std::sync::Arc;
+use std::{path::PathBuf, sync::Arc};
use theme::LoadThemes;
use ui::{div, prelude::*, Render};
use util::ResultExt as _;
@@ -159,6 +161,121 @@ impl LanguageModelTool for RollDiceTool {
}
}
+struct FileBrowserTool {
+ fs: Arc<dyn Fs>,
+ root_dir: PathBuf,
+}
+
+impl FileBrowserTool {
+ fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
+ Self { fs, root_dir }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+struct FileBrowserParams {
+ command: FileBrowserCommand,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
+enum FileBrowserCommand {
+ Ls { path: PathBuf },
+ Cat { path: PathBuf },
+}
+
+#[derive(Serialize, Deserialize)]
+enum FileBrowserOutput {
+ Ls { entries: Vec<String> },
+ Cat { content: String },
+}
+
+pub struct FileBrowserView {
+ result: Result<FileBrowserOutput>,
+}
+
+impl Render for FileBrowserView {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let Ok(output) = self.result.as_ref() else {
+ return h_flex().child("Failed to perform operation");
+ };
+
+ match output {
+ FileBrowserOutput::Ls { entries } => v_flex().children(
+ entries
+ .into_iter()
+ .map(|entry| h_flex().text_ui(cx).child(entry.clone())),
+ ),
+ FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
+ }
+ }
+}
+
+impl LanguageModelTool for FileBrowserTool {
+ type Input = FileBrowserParams;
+ type Output = FileBrowserOutput;
+ type View = FileBrowserView;
+
+ fn name(&self) -> String {
+ "file_browser".to_string()
+ }
+
+ fn description(&self) -> String {
+ "A tool for browsing the filesystem.".to_string()
+ }
+
+ fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
+ cx.spawn({
+ let fs = self.fs.clone();
+ let root_dir = self.root_dir.clone();
+ let input = input.clone();
+ |_cx| async move {
+ match input.command {
+ FileBrowserCommand::Ls { path } => {
+ let path = root_dir.join(path);
+
+ let mut output = fs.read_dir(&path).await?;
+
+ let mut entries = Vec::new();
+ while let Some(entry) = output.next().await {
+ let entry = entry?;
+ entries.push(entry.display().to_string());
+ }
+
+ Ok(FileBrowserOutput::Ls { entries })
+ }
+ FileBrowserCommand::Cat { path } => {
+ let path = root_dir.join(path);
+
+ let output = fs.load(&path).await?;
+
+ Ok(FileBrowserOutput::Cat { content: output })
+ }
+ }
+ }
+ })
+ }
+
+ fn output_view(
+ _tool_call_id: String,
+ _input: Self::Input,
+ result: Result<Self::Output>,
+ cx: &mut WindowContext,
+ ) -> gpui::View<Self::View> {
+ cx.new_view(|_cx| FileBrowserView { result })
+ }
+
+ fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
+ let Ok(output) = output else {
+ return "Failed to perform command: {input:?}".to_string();
+ };
+
+ match output {
+ FileBrowserOutput::Ls { entries } => entries.join("\n"),
+ FileBrowserOutput::Cat { content } => content.to_owned(),
+ }
+ }
+}
+
fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
@@ -189,11 +306,16 @@ fn main() {
Task::ready(()),
cx.background_executor().clone(),
));
+
+ let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
languages::init(language_registry.clone(), node_runtime, cx);
cx.spawn(|cx| async move {
cx.update(|cx| {
+ let fs = Arc::new(fs::RealFs::new(None));
+ let cwd = std::env::current_dir().expect("Failed to get current working directory");
+
cx.open_window(WindowOptions::default(), |cx| {
let mut tool_registry = ToolRegistry::new();
tool_registry
@@ -201,6 +323,11 @@ fn main() {
.context("failed to register DummyTool")
.log_err();
+ tool_registry
+ .register(FileBrowserTool::new(fs, cwd), cx)
+ .context("failed to register FileBrowserTool")
+ .log_err();
+
let tool_registry = Arc::new(tool_registry);
println!("Tools registered");
@@ -208,7 +335,7 @@ fn main() {
println!("{}", definition);
}
- cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
+ cx.new_view(|cx| Example::new(language_registry, tool_registry, user_store, cx))
});
cx.activate(true);
})
@@ -225,11 +352,13 @@ impl Example {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
+ user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
- assistant_panel: cx
- .new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
+ assistant_panel: cx.new_view(|cx| {
+ AssistantPanel::new(language_registry, tool_registry, user_store, cx)
+ }),
}
}
}
@@ -1,221 +0,0 @@
-//! This example creates a basic Chat UI for interacting with the filesystem.
-
-use anyhow::{Context as _, Result};
-use assets::Assets;
-use assistant2::AssistantPanel;
-use assistant_tooling::{LanguageModelTool, ToolRegistry};
-use client::Client;
-use fs::Fs;
-use futures::StreamExt;
-use gpui::{actions, App, AppContext, KeyBinding, Task, View, WindowOptions};
-use language::LanguageRegistry;
-use project::Project;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
-use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
-use std::path::PathBuf;
-use std::sync::Arc;
-use theme::LoadThemes;
-use ui::{div, prelude::*, Render};
-use util::ResultExt as _;
-
-actions!(example, [Quit]);
-
-struct FileBrowserTool {
- fs: Arc<dyn Fs>,
- root_dir: PathBuf,
-}
-
-impl FileBrowserTool {
- fn new(fs: Arc<dyn Fs>, root_dir: PathBuf) -> Self {
- Self { fs, root_dir }
- }
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
-struct FileBrowserParams {
- command: FileBrowserCommand,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
-enum FileBrowserCommand {
- Ls { path: PathBuf },
- Cat { path: PathBuf },
-}
-
-#[derive(Serialize, Deserialize)]
-enum FileBrowserOutput {
- Ls { entries: Vec<String> },
- Cat { content: String },
-}
-
-pub struct FileBrowserView {
- result: Result<FileBrowserOutput>,
-}
-
-impl Render for FileBrowserView {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let Ok(output) = self.result.as_ref() else {
- return h_flex().child("Failed to perform operation");
- };
-
- match output {
- FileBrowserOutput::Ls { entries } => v_flex().children(
- entries
- .into_iter()
- .map(|entry| h_flex().text_ui(cx).child(entry.clone())),
- ),
- FileBrowserOutput::Cat { content } => h_flex().child(content.clone()),
- }
- }
-}
-
-impl LanguageModelTool for FileBrowserTool {
- type Input = FileBrowserParams;
- type Output = FileBrowserOutput;
- type View = FileBrowserView;
-
- fn name(&self) -> String {
- "file_browser".to_string()
- }
-
- fn description(&self) -> String {
- "A tool for browsing the filesystem.".to_string()
- }
-
- fn execute(&self, input: &Self::Input, cx: &AppContext) -> Task<gpui::Result<Self::Output>> {
- cx.spawn({
- let fs = self.fs.clone();
- let root_dir = self.root_dir.clone();
- let input = input.clone();
- |_cx| async move {
- match input.command {
- FileBrowserCommand::Ls { path } => {
- let path = root_dir.join(path);
-
- let mut output = fs.read_dir(&path).await?;
-
- let mut entries = Vec::new();
- while let Some(entry) = output.next().await {
- let entry = entry?;
- entries.push(entry.display().to_string());
- }
-
- Ok(FileBrowserOutput::Ls { entries })
- }
- FileBrowserCommand::Cat { path } => {
- let path = root_dir.join(path);
-
- let output = fs.load(&path).await?;
-
- Ok(FileBrowserOutput::Cat { content: output })
- }
- }
- }
- })
- }
-
- fn output_view(
- _tool_call_id: String,
- _input: Self::Input,
- result: Result<Self::Output>,
- cx: &mut WindowContext,
- ) -> gpui::View<Self::View> {
- cx.new_view(|_cx| FileBrowserView { result })
- }
-
- fn format(_input: &Self::Input, output: &Result<Self::Output>) -> String {
- let Ok(output) = output else {
- return "Failed to perform command: {input:?}".to_string();
- };
-
- match output {
- FileBrowserOutput::Ls { entries } => entries.join("\n"),
- FileBrowserOutput::Cat { content } => content.to_owned(),
- }
- }
-}
-
-fn main() {
- env_logger::init();
- App::new().with_assets(Assets).run(|cx| {
- cx.bind_keys(Some(KeyBinding::new("cmd-q", Quit, None)));
- cx.on_action(|_: &Quit, cx: &mut AppContext| {
- cx.quit();
- });
-
- settings::init(cx);
- language::init(cx);
- Project::init_settings(cx);
- editor::init(cx);
- theme::init(LoadThemes::JustBase, cx);
- Assets.load_fonts(cx).unwrap();
- KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, cx).unwrap();
- client::init_settings(cx);
- release_channel::init("0.130.0", cx);
-
- let client = Client::production(cx);
- {
- let client = client.clone();
- cx.spawn(|cx| async move { client.authenticate_and_connect(false, &cx).await })
- .detach_and_log_err(cx);
- }
- assistant2::init(client.clone(), cx);
-
- let language_registry = Arc::new(LanguageRegistry::new(
- Task::ready(()),
- cx.background_executor().clone(),
- ));
- let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
- languages::init(language_registry.clone(), node_runtime, cx);
-
- cx.spawn(|cx| async move {
- cx.update(|cx| {
- let fs = Arc::new(fs::RealFs::new(None));
- let cwd = std::env::current_dir().expect("Failed to get current working directory");
-
- cx.open_window(WindowOptions::default(), |cx| {
- let mut tool_registry = ToolRegistry::new();
- tool_registry
- .register(FileBrowserTool::new(fs, cwd), cx)
- .context("failed to register FileBrowserTool")
- .log_err();
-
- let tool_registry = Arc::new(tool_registry);
-
- println!("Tools registered");
- for definition in tool_registry.definitions() {
- println!("{}", definition);
- }
-
- cx.new_view(|cx| Example::new(language_registry, tool_registry, cx))
- });
- cx.activate(true);
- })
- })
- .detach_and_log_err(cx);
- })
-}
-
-struct Example {
- assistant_panel: View<AssistantPanel>,
-}
-
-impl Example {
- fn new(
- language_registry: Arc<LanguageRegistry>,
- tool_registry: Arc<ToolRegistry>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- Self {
- assistant_panel: cx
- .new_view(|cx| AssistantPanel::new(language_registry, tool_registry, cx)),
- }
- }
-}
-
-impl Render for Example {
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl ui::prelude::IntoElement {
- div().size_full().child(self.assistant_panel.clone())
- }
-}
@@ -1,17 +1,19 @@
mod assistant_settings;
mod completion_provider;
pub mod tools;
+mod ui;
+use ::ui::{div, prelude::*, Color, ViewContext};
use anyhow::{Context, Result};
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
-use client::{proto, Client};
+use client::{proto, Client, UserStore};
use completion_provider::*;
use editor::Editor;
use feature_flags::FeatureFlagAppExt as _;
use futures::{future::join_all, StreamExt};
use gpui::{
- list, prelude::*, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
- FocusableView, ListAlignment, ListState, Render, Task, View, WeakView,
+ list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
+ ListAlignment, ListState, Model, Render, Task, View, WeakView,
};
use language::{language_settings::SoftWrap, LanguageRegistry};
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
@@ -22,7 +24,7 @@ use settings::Settings;
use std::sync::Arc;
use theme::ThemeSettings;
use tools::ProjectIndexTool;
-use ui::{popover_menu, prelude::*, ButtonLike, Color, ContextMenu, Tooltip};
+use ui::Composer;
use util::{paths::EMBEDDINGS_DIR, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
@@ -101,6 +103,8 @@ impl AssistantPanel {
(workspace.app_state().clone(), workspace.project().clone())
})?;
+ let user_store = app_state.user_store.clone();
+
cx.new_view(|cx| {
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
@@ -118,7 +122,7 @@ impl AssistantPanel {
let tool_registry = Arc::new(tool_registry);
- Self::new(app_state.languages.clone(), tool_registry, cx)
+ Self::new(app_state.languages.clone(), tool_registry, user_store, cx)
})
})
}
@@ -126,10 +130,16 @@ impl AssistantPanel {
pub fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
+ user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let chat = cx.new_view(|cx| {
- AssistantChat::new(language_registry.clone(), tool_registry.clone(), cx)
+ AssistantChat::new(
+ language_registry.clone(),
+ tool_registry.clone(),
+ user_store,
+ cx,
+ )
});
Self { width: None, chat }
@@ -174,7 +184,7 @@ impl Panel for AssistantPanel {
cx.notify();
}
- fn icon(&self, _cx: &WindowContext) -> Option<ui::IconName> {
+ fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
Some(IconName::Ai)
}
@@ -191,13 +201,7 @@ impl EventEmitter<PanelEvent> for AssistantPanel {}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.chat
- .read(cx)
- .messages
- .iter()
- .rev()
- .find_map(|msg| msg.focus_handle(cx))
- .expect("no user message in chat")
+ self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
}
}
@@ -206,6 +210,8 @@ struct AssistantChat {
messages: Vec<ChatMessage>,
list_state: ListState,
language_registry: Arc<LanguageRegistry>,
+ composer_editor: View<Editor>,
+ user_store: Model<UserStore>,
next_message_id: MessageId,
pending_completion: Option<Task<()>>,
tool_registry: Arc<ToolRegistry>,
@@ -215,6 +221,7 @@ impl AssistantChat {
fn new(
language_registry: Arc<LanguageRegistry>,
tool_registry: Arc<ToolRegistry>,
+ user_store: Model<UserStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let model = CompletionProvider::get(cx).default_model();
@@ -229,17 +236,22 @@ impl AssistantChat {
},
);
- let mut this = Self {
+ Self {
model,
messages: Vec::new(),
+ composer_editor: cx.new_view(|cx| {
+ let mut editor = Editor::auto_height(80, cx);
+ editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ editor.set_placeholder_text("Type a message to the assistant", cx);
+ editor
+ }),
list_state,
+ user_store,
language_registry,
next_message_id: MessageId(0),
pending_completion: None,
tool_registry,
- };
- this.push_new_user_message(true, cx);
- this
+ }
}
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
@@ -262,19 +274,37 @@ impl AssistantChat {
if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
if message.body.text.is_empty() {
self.pop_message(cx);
- } else {
- self.push_new_user_message(false, cx);
}
}
}
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
- let Some(focused_message_id) = self.focused_message_id(cx) else {
- log::error!("unexpected state: no user message editor is focused.");
+ // Don't allow multiple concurrent completions.
+ if self.pending_completion.is_some() {
+ cx.propagate();
return;
- };
+ }
- self.truncate_messages(focused_message_id, cx);
+ if let Some(focused_message_id) = self.focused_message_id(cx) {
+ self.truncate_messages(focused_message_id, cx);
+ } else if self.composer_editor.focus_handle(cx).is_focused(cx) {
+ let message = self.composer_editor.update(cx, |composer_editor, cx| {
+ let text = composer_editor.text(cx);
+ let id = self.next_message_id.post_inc();
+ let body = cx.new_view(|cx| {
+ let mut editor = Editor::auto_height(80, cx);
+ editor.set_text(text, cx);
+ editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ editor
+ });
+ composer_editor.clear(cx);
+ ChatMessage::User(UserMessage { id, body })
+ });
+ self.push_message(message, cx);
+ } else {
+ log::error!("unexpected state: no user message editor is focused.");
+ return;
+ }
let mode = *mode;
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
@@ -288,12 +318,8 @@ impl AssistantChat {
.log_err();
this.update(&mut cx, |this, cx| {
- let focus = this
- .user_message(focused_message_id)
- .body
- .focus_handle(cx)
- .contains_focused(cx);
- this.push_new_user_message(focus, cx);
+ let composer_focus_handle = this.composer_editor.focus_handle(cx);
+ cx.focus(&composer_focus_handle);
this.pending_completion = None;
})
.context("Failed to push new user message")
@@ -301,6 +327,10 @@ impl AssistantChat {
}));
}
+ fn can_submit(&self) -> bool {
+ self.pending_completion.is_none()
+ }
+
async fn request_completion(
this: WeakView<Self>,
mode: SubmitMode,
@@ -424,32 +454,6 @@ impl AssistantChat {
}
}
- fn user_message(&mut self, message_id: MessageId) -> &mut UserMessage {
- self.messages
- .iter_mut()
- .find_map(|message| match message {
- ChatMessage::User(user_message) if user_message.id == message_id => {
- Some(user_message)
- }
- _ => None,
- })
- .expect("User message not found")
- }
-
- fn push_new_user_message(&mut self, focus: bool, cx: &mut ViewContext<Self>) {
- let id = self.next_message_id.post_inc();
- let body = cx.new_view(|cx| {
- let mut editor = Editor::auto_height(80, cx);
- editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
- if focus {
- cx.focus_self();
- }
- editor
- });
- let message = ChatMessage::User(UserMessage { id, body });
- self.push_message(message, cx);
- }
-
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
let message = ChatMessage::Assistant(AssistantMessage {
id: self.next_message_id.post_inc(),
@@ -525,7 +529,6 @@ impl AssistantChat {
.child(div().p_2().child(Label::new("You").color(Color::Default)))
.child(
div()
- .on_action(cx.listener(Self::submit))
.p_2()
.text_color(cx.theme().colors().editor_foreground)
.font(ThemeSettings::get_global(cx).buffer_font.clone())
@@ -637,59 +640,6 @@ impl AssistantChat {
completion_messages
}
-
- fn render_model_dropdown(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
- let this = cx.view().downgrade();
- div().h_flex().justify_end().child(
- div().w_32().child(
- popover_menu("user-menu")
- .menu(move |cx| {
- ContextMenu::build(cx, |mut menu, cx| {
- for model in CompletionProvider::get(cx).available_models() {
- menu = menu.custom_entry(
- {
- let model = model.clone();
- move |_| Label::new(model.clone()).into_any_element()
- },
- {
- let this = this.clone();
- move |cx| {
- _ = this.update(cx, |this, cx| {
- this.model = model.clone();
- cx.notify();
- });
- }
- },
- );
- }
- menu
- })
- .into()
- })
- .trigger(
- ButtonLike::new("active-model")
- .child(
- h_flex()
- .w_full()
- .gap_0p5()
- .child(
- div()
- .overflow_x_hidden()
- .flex_grow()
- .whitespace_nowrap()
- .child(Label::new(self.model.clone())),
- )
- .child(div().child(
- Icon::new(IconName::ChevronDown).color(Color::Muted),
- )),
- )
- .style(ButtonStyle::Subtle)
- .tooltip(move |cx| Tooltip::text("Change Model", cx)),
- )
- .anchor(gpui::AnchorCorner::TopRight),
- ),
- )
- }
}
impl Render for AssistantChat {
@@ -699,20 +649,22 @@ impl Render for AssistantChat {
.flex_1()
.v_flex()
.key_context("AssistantChat")
+ .on_action(cx.listener(Self::submit))
.on_action(cx.listener(Self::cancel))
.text_color(Color::Default.color(cx))
- .child(self.render_model_dropdown(cx))
.child(list(self.list_state.clone()).flex_1())
- .child(
- h_flex()
- .mt_2()
- .gap_2()
- .children(self.tool_registry.status_views().iter().cloned()),
- )
+ .child(Composer::new(
+ cx.view().downgrade(),
+ self.model.clone(),
+ self.composer_editor.clone(),
+ self.user_store.read(cx).current_user(),
+ self.can_submit(),
+ self.tool_registry.clone(),
+ ))
}
}
-#[derive(Copy, Clone, Eq, PartialEq)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct MessageId(usize);
impl MessageId {
@@ -0,0 +1,3 @@
+mod composer;
+
+pub use composer::*;
@@ -0,0 +1,222 @@
+use assistant_tooling::ToolRegistry;
+use client::User;
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{FontStyle, FontWeight, TextStyle, View, WeakView, WhiteSpace};
+use settings::Settings;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{popover_menu, prelude::*, Avatar, ButtonLike, ContextMenu, Tooltip};
+
+use crate::{AssistantChat, CompletionProvider, Submit, SubmitMode};
+
+#[derive(IntoElement)]
+pub struct Composer {
+ assistant_chat: WeakView<AssistantChat>,
+ model: String,
+ editor: View<Editor>,
+ player: Option<Arc<User>>,
+ can_submit: bool,
+ tool_registry: Arc<ToolRegistry>,
+}
+
+impl Composer {
+ pub fn new(
+ assistant_chat: WeakView<AssistantChat>,
+ model: String,
+ editor: View<Editor>,
+ player: Option<Arc<User>>,
+ can_submit: bool,
+ tool_registry: Arc<ToolRegistry>,
+ ) -> Self {
+ Self {
+ assistant_chat,
+ model,
+ editor,
+ player,
+ can_submit,
+ tool_registry,
+ }
+ }
+}
+
+impl RenderOnce for Composer {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let mut player_avatar = div().size(rems(20.0 / 16.0)).into_any_element();
+ if let Some(player) = self.player.clone() {
+ player_avatar = Avatar::new(player.avatar_uri.clone())
+ .size(rems(20.0 / 16.0))
+ .into_any_element();
+ }
+
+ let font_size = rems(0.875);
+ let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
+
+ h_flex()
+ .w_full()
+ .items_start()
+ .mt_4()
+ .gap_3()
+ .child(player_avatar)
+ .child(
+ v_flex()
+ .size_full()
+ .gap_1()
+ .child(
+ v_flex()
+ .w_full()
+ .p_4()
+ .bg(cx.theme().colors().editor_background)
+ .rounded_lg()
+ .child(
+ v_flex()
+ .justify_between()
+ .w_full()
+ .gap_1()
+ .min_h(line_height * 4 + px(74.0))
+ .child({
+ let settings = ThemeSettings::get_global(cx);
+ let text_style = TextStyle {
+ color: cx.theme().colors().editor_foreground,
+ font_family: settings.buffer_font.family.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: font_size.into(),
+ font_weight: FontWeight::NORMAL,
+ font_style: FontStyle::Normal,
+ line_height: line_height.into(),
+ background_color: None,
+ underline: None,
+ strikethrough: None,
+ white_space: WhiteSpace::Normal,
+ };
+
+ EditorElement::new(
+ &self.editor,
+ EditorStyle {
+ background: cx.theme().colors().editor_background,
+ local_player: cx.theme().players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ )
+ })
+ .child(
+ h_flex()
+ .flex_none()
+ .gap_2()
+ .justify_between()
+ .w_full()
+ .child(
+ h_flex().gap_1().child(
+ // IconButton/button
+ // Toggle - if enabled, .selected(true).selected_style(IconButtonStyle::Filled)
+ //
+ // match status
+ // Tooltip::with_meta("some label explaining project index + status", "click to enable")
+ IconButton::new(
+ "add-context",
+ IconName::FileDoc,
+ )
+ .icon_color(Color::Muted),
+ ), // .child(
+ // IconButton::new(
+ // "add-context",
+ // IconName::Plus,
+ // )
+ // .icon_color(Color::Muted),
+ // ),
+ )
+ .child(
+ Button::new("send-button", "Send")
+ .style(ButtonStyle::Filled)
+ .disabled(!self.can_submit)
+ .on_click(|_, cx| {
+ cx.dispatch_action(Box::new(Submit(
+ SubmitMode::Codebase,
+ )))
+ })
+ .tooltip(|cx| {
+ Tooltip::for_action(
+ "Submit message",
+ &Submit(SubmitMode::Codebase),
+ cx,
+ )
+ }),
+ ),
+ ),
+ ),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .justify_between()
+ .child(ModelSelector::new(self.assistant_chat, self.model))
+ .children(self.tool_registry.status_views().iter().cloned()),
+ ),
+ )
+ }
+}
+
+#[derive(IntoElement)]
+struct ModelSelector {
+ assistant_chat: WeakView<AssistantChat>,
+ model: String,
+}
+
+impl ModelSelector {
+ pub fn new(assistant_chat: WeakView<AssistantChat>, model: String) -> Self {
+ Self {
+ assistant_chat,
+ model,
+ }
+ }
+}
+
+impl RenderOnce for ModelSelector {
+ fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+ popover_menu("model-switcher")
+ .menu(move |cx| {
+ ContextMenu::build(cx, |mut menu, cx| {
+ for model in CompletionProvider::get(cx).available_models() {
+ menu = menu.custom_entry(
+ {
+ let model = model.clone();
+ move |_| Label::new(model.clone()).into_any_element()
+ },
+ {
+ let assistant_chat = self.assistant_chat.clone();
+ move |cx| {
+ _ = assistant_chat.update(cx, |assistant_chat, cx| {
+ assistant_chat.model = model.clone();
+ cx.notify();
+ });
+ }
+ },
+ );
+ }
+ menu
+ })
+ .into()
+ })
+ .trigger(
+ ButtonLike::new("active-model")
+ .child(
+ h_flex()
+ .w_full()
+ .gap_0p5()
+ .child(
+ div()
+ .overflow_x_hidden()
+ .flex_grow()
+ .whitespace_nowrap()
+ .child(Label::new(self.model)),
+ )
+ .child(
+ div().child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
+ ),
+ )
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| Tooltip::text("Change Model", cx)),
+ )
+ .anchor(gpui::AnchorCorner::BottomRight)
+ }
+}