Pin message composer to the bottom of the new assistant panel (#11186)
Release Notes: - N/A --------- Co-authored-by: Marshall <marshall@zed.dev> Co-authored-by: Nate <nate@zed.dev> Co-authored-by: Kyle <kylek@zed.dev> Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
parent
f842d19b0b
commit
1c09b69384
7 changed files with 430 additions and 477 deletions
|
@ -8,11 +8,6 @@ license = "GPL-3.0-or-later"
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/assistant2.rs"
|
path = "src/assistant2.rs"
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "assistant_example"
|
|
||||||
path = "examples/assistant_example.rs"
|
|
||||||
crate-type = ["bin"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assistant_tooling.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 assets::Assets;
|
||||||
use assistant2::AssistantPanel;
|
use assistant2::AssistantPanel;
|
||||||
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
use assistant_tooling::{LanguageModelTool, ToolRegistry};
|
||||||
use client::Client;
|
use client::{Client, UserStore};
|
||||||
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Task, View, WindowOptions};
|
use fs::Fs;
|
||||||
|
use futures::StreamExt as _;
|
||||||
|
use gpui::{actions, AnyElement, App, AppContext, KeyBinding, Model, Task, View, WindowOptions};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
use settings::{KeymapFile, DEFAULT_KEYMAP_PATH};
|
||||||
use std::sync::Arc;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
use theme::LoadThemes;
|
use theme::LoadThemes;
|
||||||
use ui::{div, prelude::*, Render};
|
use ui::{div, prelude::*, Render};
|
||||||
use util::ResultExt as _;
|
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() {
|
fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
App::new().with_assets(Assets).run(|cx| {
|
App::new().with_assets(Assets).run(|cx| {
|
||||||
|
@ -189,11 +306,16 @@ fn main() {
|
||||||
Task::ready(()),
|
Task::ready(()),
|
||||||
cx.background_executor().clone(),
|
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());
|
let node_runtime = node_runtime::RealNodeRuntime::new(client.http_client());
|
||||||
languages::init(language_registry.clone(), node_runtime, cx);
|
languages::init(language_registry.clone(), node_runtime, cx);
|
||||||
|
|
||||||
cx.spawn(|cx| async move {
|
cx.spawn(|cx| async move {
|
||||||
cx.update(|cx| {
|
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| {
|
cx.open_window(WindowOptions::default(), |cx| {
|
||||||
let mut tool_registry = ToolRegistry::new();
|
let mut tool_registry = ToolRegistry::new();
|
||||||
tool_registry
|
tool_registry
|
||||||
|
@ -201,6 +323,11 @@ fn main() {
|
||||||
.context("failed to register DummyTool")
|
.context("failed to register DummyTool")
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
|
tool_registry
|
||||||
|
.register(FileBrowserTool::new(fs, cwd), cx)
|
||||||
|
.context("failed to register FileBrowserTool")
|
||||||
|
.log_err();
|
||||||
|
|
||||||
let tool_registry = Arc::new(tool_registry);
|
let tool_registry = Arc::new(tool_registry);
|
||||||
|
|
||||||
println!("Tools registered");
|
println!("Tools registered");
|
||||||
|
@ -208,7 +335,7 @@ fn main() {
|
||||||
println!("{}", definition);
|
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);
|
cx.activate(true);
|
||||||
})
|
})
|
||||||
|
@ -225,11 +352,13 @@ impl Example {
|
||||||
fn new(
|
fn new(
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
tool_registry: Arc<ToolRegistry>,
|
tool_registry: Arc<ToolRegistry>,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
assistant_panel: cx
|
assistant_panel: cx.new_view(|cx| {
|
||||||
.new_view(|cx| AssistantPanel::new(language_registry, tool_registry, 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 assistant_settings;
|
||||||
mod completion_provider;
|
mod completion_provider;
|
||||||
pub mod tools;
|
pub mod tools;
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
use ::ui::{div, prelude::*, Color, ViewContext};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
|
use assistant_tooling::{ToolFunctionCall, ToolRegistry};
|
||||||
use client::{proto, Client};
|
use client::{proto, Client, UserStore};
|
||||||
use completion_provider::*;
|
use completion_provider::*;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use feature_flags::FeatureFlagAppExt as _;
|
use feature_flags::FeatureFlagAppExt as _;
|
||||||
use futures::{future::join_all, StreamExt};
|
use futures::{future::join_all, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
list, prelude::*, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
|
list, AnyElement, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusableView,
|
||||||
FocusableView, ListAlignment, ListState, Render, Task, View, WeakView,
|
ListAlignment, ListState, Model, Render, Task, View, WeakView,
|
||||||
};
|
};
|
||||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||||
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
|
use open_ai::{FunctionContent, ToolCall, ToolCallContent};
|
||||||
|
@ -22,7 +24,7 @@ use settings::Settings;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use tools::ProjectIndexTool;
|
use tools::ProjectIndexTool;
|
||||||
use ui::{popover_menu, prelude::*, ButtonLike, Color, ContextMenu, Tooltip};
|
use ui::Composer;
|
||||||
use util::{paths::EMBEDDINGS_DIR, ResultExt};
|
use util::{paths::EMBEDDINGS_DIR, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
|
@ -101,6 +103,8 @@ impl AssistantPanel {
|
||||||
(workspace.app_state().clone(), workspace.project().clone())
|
(workspace.app_state().clone(), workspace.project().clone())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let user_store = app_state.user_store.clone();
|
||||||
|
|
||||||
cx.new_view(|cx| {
|
cx.new_view(|cx| {
|
||||||
// todo!("this will panic if the semantic index failed to load or has not loaded yet")
|
// 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| {
|
let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
|
||||||
|
@ -118,7 +122,7 @@ impl AssistantPanel {
|
||||||
|
|
||||||
let tool_registry = Arc::new(tool_registry);
|
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(
|
pub fn new(
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
tool_registry: Arc<ToolRegistry>,
|
tool_registry: Arc<ToolRegistry>,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let chat = cx.new_view(|cx| {
|
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 }
|
Self { width: None, chat }
|
||||||
|
@ -174,7 +184,7 @@ impl Panel for AssistantPanel {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(&self, _cx: &WindowContext) -> Option<ui::IconName> {
|
fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
|
||||||
Some(IconName::Ai)
|
Some(IconName::Ai)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,13 +201,7 @@ impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||||
|
|
||||||
impl FocusableView for AssistantPanel {
|
impl FocusableView for AssistantPanel {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.chat
|
self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
|
||||||
.read(cx)
|
|
||||||
.messages
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.find_map(|msg| msg.focus_handle(cx))
|
|
||||||
.expect("no user message in chat")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,6 +210,8 @@ struct AssistantChat {
|
||||||
messages: Vec<ChatMessage>,
|
messages: Vec<ChatMessage>,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
composer_editor: View<Editor>,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
next_message_id: MessageId,
|
next_message_id: MessageId,
|
||||||
pending_completion: Option<Task<()>>,
|
pending_completion: Option<Task<()>>,
|
||||||
tool_registry: Arc<ToolRegistry>,
|
tool_registry: Arc<ToolRegistry>,
|
||||||
|
@ -215,6 +221,7 @@ impl AssistantChat {
|
||||||
fn new(
|
fn new(
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
tool_registry: Arc<ToolRegistry>,
|
tool_registry: Arc<ToolRegistry>,
|
||||||
|
user_store: Model<UserStore>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let model = CompletionProvider::get(cx).default_model();
|
let model = CompletionProvider::get(cx).default_model();
|
||||||
|
@ -229,17 +236,22 @@ impl AssistantChat {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut this = Self {
|
Self {
|
||||||
model,
|
model,
|
||||||
messages: Vec::new(),
|
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,
|
list_state,
|
||||||
|
user_store,
|
||||||
language_registry,
|
language_registry,
|
||||||
next_message_id: MessageId(0),
|
next_message_id: MessageId(0),
|
||||||
pending_completion: None,
|
pending_completion: None,
|
||||||
tool_registry,
|
tool_registry,
|
||||||
};
|
}
|
||||||
this.push_new_user_message(true, cx);
|
|
||||||
this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
|
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 let Some(ChatMessage::Assistant(message)) = self.messages.last() {
|
||||||
if message.body.text.is_empty() {
|
if message.body.text.is_empty() {
|
||||||
self.pop_message(cx);
|
self.pop_message(cx);
|
||||||
} else {
|
|
||||||
self.push_new_user_message(false, cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
|
fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
|
||||||
let Some(focused_message_id) = self.focused_message_id(cx) else {
|
// Don't allow multiple concurrent completions.
|
||||||
|
if self.pending_completion.is_some() {
|
||||||
|
cx.propagate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
log::error!("unexpected state: no user message editor is focused.");
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
self.truncate_messages(focused_message_id, cx);
|
|
||||||
|
|
||||||
let mode = *mode;
|
let mode = *mode;
|
||||||
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
|
self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
|
||||||
|
@ -288,12 +318,8 @@ impl AssistantChat {
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
let focus = this
|
let composer_focus_handle = this.composer_editor.focus_handle(cx);
|
||||||
.user_message(focused_message_id)
|
cx.focus(&composer_focus_handle);
|
||||||
.body
|
|
||||||
.focus_handle(cx)
|
|
||||||
.contains_focused(cx);
|
|
||||||
this.push_new_user_message(focus, cx);
|
|
||||||
this.pending_completion = None;
|
this.pending_completion = None;
|
||||||
})
|
})
|
||||||
.context("Failed to push new user message")
|
.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(
|
async fn request_completion(
|
||||||
this: WeakView<Self>,
|
this: WeakView<Self>,
|
||||||
mode: SubmitMode,
|
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>) {
|
fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
let message = ChatMessage::Assistant(AssistantMessage {
|
let message = ChatMessage::Assistant(AssistantMessage {
|
||||||
id: self.next_message_id.post_inc(),
|
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().p_2().child(Label::new("You").color(Color::Default)))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.on_action(cx.listener(Self::submit))
|
|
||||||
.p_2()
|
.p_2()
|
||||||
.text_color(cx.theme().colors().editor_foreground)
|
.text_color(cx.theme().colors().editor_foreground)
|
||||||
.font(ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
|
@ -637,59 +640,6 @@ impl AssistantChat {
|
||||||
|
|
||||||
completion_messages
|
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 {
|
impl Render for AssistantChat {
|
||||||
|
@ -699,20 +649,22 @@ impl Render for AssistantChat {
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.v_flex()
|
.v_flex()
|
||||||
.key_context("AssistantChat")
|
.key_context("AssistantChat")
|
||||||
|
.on_action(cx.listener(Self::submit))
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.text_color(Color::Default.color(cx))
|
.text_color(Color::Default.color(cx))
|
||||||
.child(self.render_model_dropdown(cx))
|
|
||||||
.child(list(self.list_state.clone()).flex_1())
|
.child(list(self.list_state.clone()).flex_1())
|
||||||
.child(
|
.child(Composer::new(
|
||||||
h_flex()
|
cx.view().downgrade(),
|
||||||
.mt_2()
|
self.model.clone(),
|
||||||
.gap_2()
|
self.composer_editor.clone(),
|
||||||
.children(self.tool_registry.status_views().iter().cloned()),
|
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);
|
struct MessageId(usize);
|
||||||
|
|
||||||
impl MessageId {
|
impl MessageId {
|
||||||
|
|
3
crates/assistant2/src/ui.rs
Normal file
3
crates/assistant2/src/ui.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod composer;
|
||||||
|
|
||||||
|
pub use composer::*;
|
222
crates/assistant2/src/ui/composer.rs
Normal file
222
crates/assistant2/src/ui/composer.rs
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue