Inline assistant v2 (#21828)
This is behind the Assistant v2 feature flag. As @maxdeviant and I discussed, the state is currently decoupled from the Assistant Panel's state, although in the future we plan to introduce a way to refer to conversations from the panel. Also, we're intentionally duplicating some code with the v2 panel right now; the plan is to do a future PR to make them share code more. https://github.com/user-attachments/assets/bb163bd3-a02d-4a91-8f8f-2a8e60acbc34 It doesn't include the terminal inline assistant, which will be in a separate PR. Release Notes: - N/A
This commit is contained in:
parent
937186da12
commit
c594ccb0af
8 changed files with 6392 additions and 589 deletions
1179
Cargo.lock
generated
1179
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -13,30 +13,51 @@ path = "src/assistant.rs"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anthropic = { workspace = true, features = ["schemars"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
assets.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
chrono.workspace = true
|
async-watch.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
context_server.workspace = true
|
context_server.workspace = true
|
||||||
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
handlebars.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
language_model_selector.workspace = true
|
language_model_selector.workspace = true
|
||||||
language_models.workspace = true
|
language_models.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
lsp.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
|
menu.workspace = true
|
||||||
|
multi_buffer.workspace = true
|
||||||
|
ollama = { workspace = true, features = ["schemars"] }
|
||||||
|
open_ai = { workspace = true, features = ["schemars"] }
|
||||||
|
ordered-float.workspace = true
|
||||||
|
paths.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
proto.workspace = true
|
proto.workspace = true
|
||||||
|
rope.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
serde_json_lenient.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
similar.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
telemetry_events.workspace = true
|
||||||
|
terminal_view.workspace = true
|
||||||
|
text.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
time_format.workspace = true
|
time_format.workspace = true
|
||||||
|
@ -45,3 +66,8 @@ unindent.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
zed_actions.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
mod active_thread;
|
mod active_thread;
|
||||||
mod assistant_panel;
|
mod assistant_panel;
|
||||||
|
mod assistant_settings;
|
||||||
mod context;
|
mod context;
|
||||||
mod context_picker;
|
mod context_picker;
|
||||||
|
mod inline_assistant;
|
||||||
mod message_editor;
|
mod message_editor;
|
||||||
|
mod prompts;
|
||||||
|
mod streaming_diff;
|
||||||
mod thread;
|
mod thread;
|
||||||
mod thread_history;
|
mod thread_history;
|
||||||
mod thread_store;
|
mod thread_store;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use assistant_settings::AssistantSettings;
|
||||||
|
use client::Client;
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||||
|
use fs::Fs;
|
||||||
use gpui::{actions, AppContext};
|
use gpui::{actions, AppContext};
|
||||||
|
use prompts::PromptLoadingParams;
|
||||||
|
use settings::Settings as _;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
pub use crate::assistant_panel::AssistantPanel;
|
pub use crate::assistant_panel::AssistantPanel;
|
||||||
|
|
||||||
|
@ -21,15 +33,37 @@ actions!(
|
||||||
NewThread,
|
NewThread,
|
||||||
ToggleModelSelector,
|
ToggleModelSelector,
|
||||||
OpenHistory,
|
OpenHistory,
|
||||||
Chat
|
Chat,
|
||||||
|
ToggleInlineAssist,
|
||||||
|
CycleNextInlineAssist,
|
||||||
|
CyclePreviousInlineAssist
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const NAMESPACE: &str = "assistant2";
|
const NAMESPACE: &str = "assistant2";
|
||||||
|
|
||||||
/// Initializes the `assistant2` crate.
|
/// Initializes the `assistant2` crate.
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
|
||||||
|
AssistantSettings::register(cx);
|
||||||
assistant_panel::init(cx);
|
assistant_panel::init(cx);
|
||||||
|
|
||||||
|
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||||
|
fs: fs.clone(),
|
||||||
|
repo_path: stdout_is_a_pty
|
||||||
|
.then(|| std::env::current_dir().log_err())
|
||||||
|
.flatten(),
|
||||||
|
cx,
|
||||||
|
}))
|
||||||
|
.log_err()
|
||||||
|
.map(Arc::new)
|
||||||
|
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||||
|
inline_assistant::init(
|
||||||
|
fs.clone(),
|
||||||
|
prompt_builder.clone(),
|
||||||
|
client.telemetry().clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
feature_gate_assistant2_actions(cx);
|
feature_gate_assistant2_actions(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
485
crates/assistant2/src/assistant_settings.rs
Normal file
485
crates/assistant2/src/assistant_settings.rs
Normal file
|
@ -0,0 +1,485 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ::open_ai::Model as OpenAiModel;
|
||||||
|
use anthropic::Model as AnthropicModel;
|
||||||
|
use gpui::Pixels;
|
||||||
|
use language_model::{CloudModel, LanguageModel};
|
||||||
|
use ollama::Model as OllamaModel;
|
||||||
|
use schemars::{schema::Schema, JsonSchema};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::{Settings, SettingsSources};
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum AssistantDockPosition {
|
||||||
|
Left,
|
||||||
|
#[default]
|
||||||
|
Right,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
|
#[serde(tag = "name", rename_all = "snake_case")]
|
||||||
|
pub enum AssistantProviderContentV1 {
|
||||||
|
#[serde(rename = "zed.dev")]
|
||||||
|
ZedDotDev { default_model: Option<CloudModel> },
|
||||||
|
#[serde(rename = "openai")]
|
||||||
|
OpenAi {
|
||||||
|
default_model: Option<OpenAiModel>,
|
||||||
|
api_url: Option<String>,
|
||||||
|
available_models: Option<Vec<OpenAiModel>>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "anthropic")]
|
||||||
|
Anthropic {
|
||||||
|
default_model: Option<AnthropicModel>,
|
||||||
|
api_url: Option<String>,
|
||||||
|
},
|
||||||
|
#[serde(rename = "ollama")]
|
||||||
|
Ollama {
|
||||||
|
default_model: Option<OllamaModel>,
|
||||||
|
api_url: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct AssistantSettings {
|
||||||
|
pub enabled: bool,
|
||||||
|
pub button: bool,
|
||||||
|
pub dock: AssistantDockPosition,
|
||||||
|
pub default_width: Pixels,
|
||||||
|
pub default_height: Pixels,
|
||||||
|
pub default_model: LanguageModelSelection,
|
||||||
|
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||||
|
pub using_outdated_settings_version: bool,
|
||||||
|
pub enable_experimental_live_diffs: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assistant panel settings
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum AssistantSettingsContent {
|
||||||
|
Versioned(VersionedAssistantSettingsContent),
|
||||||
|
Legacy(LegacyAssistantSettingsContent),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonSchema for AssistantSettingsContent {
|
||||||
|
fn schema_name() -> String {
|
||||||
|
VersionedAssistantSettingsContent::schema_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||||
|
VersionedAssistantSettingsContent::json_schema(gen)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_referenceable() -> bool {
|
||||||
|
VersionedAssistantSettingsContent::is_referenceable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AssistantSettingsContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantSettingsContent {
|
||||||
|
pub fn is_version_outdated(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||||
|
VersionedAssistantSettingsContent::V1(_) => true,
|
||||||
|
VersionedAssistantSettingsContent::V2(_) => false,
|
||||||
|
},
|
||||||
|
AssistantSettingsContent::Legacy(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||||
|
match self {
|
||||||
|
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||||
|
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||||
|
enabled: settings.enabled,
|
||||||
|
button: settings.button,
|
||||||
|
dock: settings.dock,
|
||||||
|
default_width: settings.default_width,
|
||||||
|
default_height: settings.default_width,
|
||||||
|
default_model: settings
|
||||||
|
.provider
|
||||||
|
.clone()
|
||||||
|
.and_then(|provider| match provider {
|
||||||
|
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||||
|
default_model.map(|model| LanguageModelSelection {
|
||||||
|
provider: "zed.dev".to_string(),
|
||||||
|
model: model.id().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||||
|
default_model.map(|model| LanguageModelSelection {
|
||||||
|
provider: "openai".to_string(),
|
||||||
|
model: model.id().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||||
|
default_model.map(|model| LanguageModelSelection {
|
||||||
|
provider: "anthropic".to_string(),
|
||||||
|
model: model.id().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||||
|
default_model.map(|model| LanguageModelSelection {
|
||||||
|
provider: "ollama".to_string(),
|
||||||
|
model: model.id().to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
inline_alternatives: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
|
},
|
||||||
|
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||||
|
},
|
||||||
|
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||||
|
enabled: None,
|
||||||
|
button: settings.button,
|
||||||
|
dock: settings.dock,
|
||||||
|
default_width: settings.default_width,
|
||||||
|
default_height: settings.default_height,
|
||||||
|
default_model: Some(LanguageModelSelection {
|
||||||
|
provider: "openai".to_string(),
|
||||||
|
model: settings
|
||||||
|
.default_open_ai_model
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.id()
|
||||||
|
.to_string(),
|
||||||
|
}),
|
||||||
|
inline_alternatives: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||||
|
let model = language_model.id().0.to_string();
|
||||||
|
let provider = language_model.provider_id().0.to_string();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||||
|
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||||
|
"zed.dev" => {
|
||||||
|
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||||
|
}
|
||||||
|
"anthropic" => {
|
||||||
|
let api_url = match &settings.provider {
|
||||||
|
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||||
|
api_url.clone()
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||||
|
default_model: AnthropicModel::from_id(&model).ok(),
|
||||||
|
api_url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"ollama" => {
|
||||||
|
let api_url = match &settings.provider {
|
||||||
|
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||||
|
api_url.clone()
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||||
|
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||||
|
api_url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
"openai" => {
|
||||||
|
let (api_url, available_models) = match &settings.provider {
|
||||||
|
Some(AssistantProviderContentV1::OpenAi {
|
||||||
|
api_url,
|
||||||
|
available_models,
|
||||||
|
..
|
||||||
|
}) => (api_url.clone(), available_models.clone()),
|
||||||
|
_ => (None, None),
|
||||||
|
};
|
||||||
|
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||||
|
default_model: OpenAiModel::from_id(&model).ok(),
|
||||||
|
api_url,
|
||||||
|
available_models,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
VersionedAssistantSettingsContent::V2(settings) => {
|
||||||
|
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AssistantSettingsContent::Legacy(settings) => {
|
||||||
|
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||||
|
settings.default_open_ai_model = Some(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
#[serde(tag = "version")]
|
||||||
|
pub enum VersionedAssistantSettingsContent {
|
||||||
|
#[serde(rename = "1")]
|
||||||
|
V1(AssistantSettingsContentV1),
|
||||||
|
#[serde(rename = "2")]
|
||||||
|
V2(AssistantSettingsContentV2),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VersionedAssistantSettingsContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::V2(AssistantSettingsContentV2 {
|
||||||
|
enabled: None,
|
||||||
|
button: None,
|
||||||
|
dock: None,
|
||||||
|
default_width: None,
|
||||||
|
default_height: None,
|
||||||
|
default_model: None,
|
||||||
|
inline_alternatives: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
pub struct AssistantSettingsContentV2 {
|
||||||
|
/// Whether the Assistant is enabled.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
enabled: Option<bool>,
|
||||||
|
/// Whether to show the assistant panel button in the status bar.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
button: Option<bool>,
|
||||||
|
/// Where to dock the assistant.
|
||||||
|
///
|
||||||
|
/// Default: right
|
||||||
|
dock: Option<AssistantDockPosition>,
|
||||||
|
/// Default width in pixels when the assistant is docked to the left or right.
|
||||||
|
///
|
||||||
|
/// Default: 640
|
||||||
|
default_width: Option<f32>,
|
||||||
|
/// Default height in pixels when the assistant is docked to the bottom.
|
||||||
|
///
|
||||||
|
/// Default: 320
|
||||||
|
default_height: Option<f32>,
|
||||||
|
/// The default model to use when creating new chats.
|
||||||
|
default_model: Option<LanguageModelSelection>,
|
||||||
|
/// Additional models with which to generate alternatives when performing inline assists.
|
||||||
|
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||||
|
/// Enable experimental live diffs in the assistant panel.
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
enable_experimental_live_diffs: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
|
pub struct LanguageModelSelection {
|
||||||
|
#[schemars(schema_with = "providers_schema")]
|
||||||
|
pub provider: String,
|
||||||
|
pub model: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
|
schemars::schema::SchemaObject {
|
||||||
|
enum_values: Some(vec![
|
||||||
|
"anthropic".into(),
|
||||||
|
"google".into(),
|
||||||
|
"ollama".into(),
|
||||||
|
"openai".into(),
|
||||||
|
"zed.dev".into(),
|
||||||
|
"copilot_chat".into(),
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LanguageModelSelection {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
provider: "openai".to_string(),
|
||||||
|
model: "gpt-4".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
pub struct AssistantSettingsContentV1 {
|
||||||
|
/// Whether the Assistant is enabled.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
enabled: Option<bool>,
|
||||||
|
/// Whether to show the assistant panel button in the status bar.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
button: Option<bool>,
|
||||||
|
/// Where to dock the assistant.
|
||||||
|
///
|
||||||
|
/// Default: right
|
||||||
|
dock: Option<AssistantDockPosition>,
|
||||||
|
/// Default width in pixels when the assistant is docked to the left or right.
|
||||||
|
///
|
||||||
|
/// Default: 640
|
||||||
|
default_width: Option<f32>,
|
||||||
|
/// Default height in pixels when the assistant is docked to the bottom.
|
||||||
|
///
|
||||||
|
/// Default: 320
|
||||||
|
default_height: Option<f32>,
|
||||||
|
/// The provider of the assistant service.
|
||||||
|
///
|
||||||
|
/// This can be "openai", "anthropic", "ollama", "zed.dev"
|
||||||
|
/// each with their respective default models and configurations.
|
||||||
|
provider: Option<AssistantProviderContentV1>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
|
pub struct LegacyAssistantSettingsContent {
|
||||||
|
/// Whether to show the assistant panel button in the status bar.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
pub button: Option<bool>,
|
||||||
|
/// Where to dock the assistant.
|
||||||
|
///
|
||||||
|
/// Default: right
|
||||||
|
pub dock: Option<AssistantDockPosition>,
|
||||||
|
/// Default width in pixels when the assistant is docked to the left or right.
|
||||||
|
///
|
||||||
|
/// Default: 640
|
||||||
|
pub default_width: Option<f32>,
|
||||||
|
/// Default height in pixels when the assistant is docked to the bottom.
|
||||||
|
///
|
||||||
|
/// Default: 320
|
||||||
|
pub default_height: Option<f32>,
|
||||||
|
/// The default OpenAI model to use when creating new chats.
|
||||||
|
///
|
||||||
|
/// Default: gpt-4-1106-preview
|
||||||
|
pub default_open_ai_model: Option<OpenAiModel>,
|
||||||
|
/// OpenAI API base URL to use when creating new chats.
|
||||||
|
///
|
||||||
|
/// Default: https://api.openai.com/v1
|
||||||
|
pub openai_api_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for AssistantSettings {
|
||||||
|
const KEY: Option<&'static str> = Some("assistant");
|
||||||
|
|
||||||
|
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||||
|
|
||||||
|
type FileContent = AssistantSettingsContent;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
sources: SettingsSources<Self::FileContent>,
|
||||||
|
_: &mut gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let mut settings = AssistantSettings::default();
|
||||||
|
|
||||||
|
for value in sources.defaults_and_customizations() {
|
||||||
|
if value.is_version_outdated() {
|
||||||
|
settings.using_outdated_settings_version = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = value.upgrade();
|
||||||
|
merge(&mut settings.enabled, value.enabled);
|
||||||
|
merge(&mut settings.button, value.button);
|
||||||
|
merge(&mut settings.dock, value.dock);
|
||||||
|
merge(
|
||||||
|
&mut settings.default_width,
|
||||||
|
value.default_width.map(Into::into),
|
||||||
|
);
|
||||||
|
merge(
|
||||||
|
&mut settings.default_height,
|
||||||
|
value.default_height.map(Into::into),
|
||||||
|
);
|
||||||
|
merge(&mut settings.default_model, value.default_model);
|
||||||
|
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||||
|
merge(
|
||||||
|
&mut settings.enable_experimental_live_diffs,
|
||||||
|
value.enable_experimental_live_diffs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||||
|
if let Some(value) = value {
|
||||||
|
*target = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use fs::Fs;
|
||||||
|
use gpui::{ReadGlobal, TestAppContext};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||||
|
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||||
|
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
let test_settings = settings::SettingsStore::test(cx);
|
||||||
|
cx.set_global(test_settings);
|
||||||
|
AssistantSettings::register(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||||
|
assert_eq!(
|
||||||
|
AssistantSettings::get_global(cx).default_model,
|
||||||
|
LanguageModelSelection {
|
||||||
|
provider: "zed.dev".into(),
|
||||||
|
model: "claude-3-5-sonnet".into(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||||
|
fs.clone(),
|
||||||
|
|settings, _| {
|
||||||
|
*settings = AssistantSettingsContent::Versioned(
|
||||||
|
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||||
|
default_model: Some(LanguageModelSelection {
|
||||||
|
provider: "test-provider".into(),
|
||||||
|
model: "gpt-99".into(),
|
||||||
|
}),
|
||||||
|
inline_alternatives: None,
|
||||||
|
enabled: None,
|
||||||
|
button: None,
|
||||||
|
dock: None,
|
||||||
|
default_width: None,
|
||||||
|
default_height: None,
|
||||||
|
enable_experimental_live_diffs: None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||||
|
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AssistantSettingsTest {
|
||||||
|
assistant: AssistantSettingsContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
let assistant_settings: AssistantSettingsTest =
|
||||||
|
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||||
|
|
||||||
|
assert!(!assistant_settings.assistant.is_version_outdated());
|
||||||
|
}
|
||||||
|
}
|
3851
crates/assistant2/src/inline_assistant.rs
Normal file
3851
crates/assistant2/src/inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load diff
291
crates/assistant2/src/prompts.rs
Normal file
291
crates/assistant2/src/prompts.rs
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use assets::Assets;
|
||||||
|
use fs::Fs;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use gpui::AssetSource;
|
||||||
|
use handlebars::{Handlebars, RenderError};
|
||||||
|
use language::{BufferSnapshot, LanguageName, Point};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
use text::LineEnding;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ContentPromptDiagnosticContext {
|
||||||
|
pub line_number: usize,
|
||||||
|
pub error_message: String,
|
||||||
|
pub code_content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ContentPromptContext {
|
||||||
|
pub content_type: String,
|
||||||
|
pub language_name: Option<String>,
|
||||||
|
pub is_insert: bool,
|
||||||
|
pub is_truncated: bool,
|
||||||
|
pub document_content: String,
|
||||||
|
pub user_prompt: String,
|
||||||
|
pub rewrite_section: Option<String>,
|
||||||
|
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TerminalAssistantPromptContext {
|
||||||
|
pub os: String,
|
||||||
|
pub arch: String,
|
||||||
|
pub shell: Option<String>,
|
||||||
|
pub working_directory: Option<String>,
|
||||||
|
pub latest_output: Vec<String>,
|
||||||
|
pub user_prompt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ProjectSlashCommandPromptContext {
|
||||||
|
pub context_buffer: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PromptLoadingParams<'a> {
|
||||||
|
pub fs: Arc<dyn Fs>,
|
||||||
|
pub repo_path: Option<PathBuf>,
|
||||||
|
pub cx: &'a gpui::AppContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PromptBuilder {
|
||||||
|
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptBuilder {
|
||||||
|
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||||
|
let mut handlebars = Handlebars::new();
|
||||||
|
Self::register_built_in_templates(&mut handlebars)?;
|
||||||
|
|
||||||
|
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||||
|
|
||||||
|
if let Some(params) = loading_params {
|
||||||
|
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { handlebars })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watches the filesystem for changes to prompt template overrides.
|
||||||
|
///
|
||||||
|
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||||
|
/// an initial scan of the directory and registers any existing template overrides.
|
||||||
|
/// Then it continuously monitors for changes, reloading templates as they are
|
||||||
|
/// modified or added.
|
||||||
|
///
|
||||||
|
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||||
|
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||||
|
/// directory to be recreated.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||||
|
/// and application context.
|
||||||
|
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||||
|
fn watch_fs_for_template_overrides(
|
||||||
|
params: PromptLoadingParams,
|
||||||
|
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||||
|
) {
|
||||||
|
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||||
|
params.cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let Some(parent_dir) = templates_dir.parent() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut found_dir_once = false;
|
||||||
|
loop {
|
||||||
|
// Check if the templates directory exists and handle its status
|
||||||
|
// If it exists, log its presence and check if it's a symlink
|
||||||
|
// If it doesn't exist:
|
||||||
|
// - Log that we're using built-in prompts
|
||||||
|
// - Check if it's a broken symlink and log if so
|
||||||
|
// - Set up a watcher to detect when it's created
|
||||||
|
// After the first check, set the `found_dir_once` flag
|
||||||
|
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||||
|
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||||
|
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||||
|
if dir_status {
|
||||||
|
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||||
|
if let Some(target) = symlink_status {
|
||||||
|
log_message.push_str(" -> ");
|
||||||
|
log_message.push_str(&target.display().to_string());
|
||||||
|
}
|
||||||
|
log::info!("{}.", log_message);
|
||||||
|
} else {
|
||||||
|
if !found_dir_once {
|
||||||
|
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||||
|
if let Some(target) = symlink_status {
|
||||||
|
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.fs.is_dir(parent_dir).await {
|
||||||
|
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||||
|
while let Some(changed_paths) = changes.next().await {
|
||||||
|
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||||
|
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||||
|
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||||
|
log_message.push_str(" -> ");
|
||||||
|
log_message.push_str(&target.display().to_string());
|
||||||
|
}
|
||||||
|
log::info!("{}.", log_message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
found_dir_once = true;
|
||||||
|
|
||||||
|
// Initial scan of the prompt overrides directory
|
||||||
|
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||||
|
while let Some(Ok(file_path)) = entries.next().await {
|
||||||
|
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||||
|
if let Ok(content) = params.fs.load(&file_path).await {
|
||||||
|
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||||
|
log::debug!("Registering prompt template override: {}", file_name);
|
||||||
|
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch both the parent directory and the template overrides directory:
|
||||||
|
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||||
|
// - Monitor the template overrides directory to re-register templates when they change.
|
||||||
|
// Combine both watch streams into a single stream.
|
||||||
|
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||||
|
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||||
|
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||||
|
|
||||||
|
while let Some(changed_paths) = combined_changes.next().await {
|
||||||
|
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
|
||||||
|
if !params.fs.is_dir(&templates_dir).await {
|
||||||
|
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||||
|
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for event in changed_paths {
|
||||||
|
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
|
||||||
|
log::info!("Reloading prompt template override: {}", event.path.display());
|
||||||
|
if let Some(content) = params.fs.load(&event.path).await.log_err() {
|
||||||
|
let file_name = event.path.file_stem().unwrap().to_string_lossy();
|
||||||
|
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(watcher);
|
||||||
|
drop(parent_watcher);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||||
|
for path in Assets.list("prompts")? {
|
||||||
|
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||||
|
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||||
|
log::debug!("Registering built-in prompt template: {}", id);
|
||||||
|
let prompt = String::from_utf8_lossy(prompt.as_ref());
|
||||||
|
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_inline_transformation_prompt(
|
||||||
|
&self,
|
||||||
|
user_prompt: String,
|
||||||
|
language_name: Option<&LanguageName>,
|
||||||
|
buffer: BufferSnapshot,
|
||||||
|
range: Range<usize>,
|
||||||
|
) -> Result<String, RenderError> {
|
||||||
|
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
|
||||||
|
None | Some("Markdown" | "Plain Text") => "text",
|
||||||
|
Some(_) => "code",
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_CTX: usize = 50000;
|
||||||
|
let is_insert = range.is_empty();
|
||||||
|
let mut is_truncated = false;
|
||||||
|
|
||||||
|
let before_range = 0..range.start;
|
||||||
|
let truncated_before = if before_range.len() > MAX_CTX {
|
||||||
|
is_truncated = true;
|
||||||
|
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
|
||||||
|
start..range.start
|
||||||
|
} else {
|
||||||
|
before_range
|
||||||
|
};
|
||||||
|
|
||||||
|
let after_range = range.end..buffer.len();
|
||||||
|
let truncated_after = if after_range.len() > MAX_CTX {
|
||||||
|
is_truncated = true;
|
||||||
|
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
|
||||||
|
range.end..end
|
||||||
|
} else {
|
||||||
|
after_range
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut document_content = String::new();
|
||||||
|
for chunk in buffer.text_for_range(truncated_before) {
|
||||||
|
document_content.push_str(chunk);
|
||||||
|
}
|
||||||
|
if is_insert {
|
||||||
|
document_content.push_str("<insert_here></insert_here>");
|
||||||
|
} else {
|
||||||
|
document_content.push_str("<rewrite_this>\n");
|
||||||
|
for chunk in buffer.text_for_range(range.clone()) {
|
||||||
|
document_content.push_str(chunk);
|
||||||
|
}
|
||||||
|
document_content.push_str("\n</rewrite_this>");
|
||||||
|
}
|
||||||
|
for chunk in buffer.text_for_range(truncated_after) {
|
||||||
|
document_content.push_str(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rewrite_section = if !is_insert {
|
||||||
|
let mut section = String::new();
|
||||||
|
for chunk in buffer.text_for_range(range.clone()) {
|
||||||
|
section.push_str(chunk);
|
||||||
|
}
|
||||||
|
Some(section)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
|
||||||
|
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
|
||||||
|
.map(|entry| {
|
||||||
|
let start = entry.range.start;
|
||||||
|
ContentPromptDiagnosticContext {
|
||||||
|
line_number: (start.row + 1) as usize,
|
||||||
|
error_message: entry.diagnostic.message.clone(),
|
||||||
|
code_content: buffer.text_for_range(entry.range.clone()).collect(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let context = ContentPromptContext {
|
||||||
|
content_type: content_type.to_string(),
|
||||||
|
language_name: language_name.map(|s| s.to_string()),
|
||||||
|
is_insert,
|
||||||
|
is_truncated,
|
||||||
|
document_content,
|
||||||
|
user_prompt,
|
||||||
|
rewrite_section,
|
||||||
|
diagnostic_errors,
|
||||||
|
};
|
||||||
|
self.handlebars.lock().render("content_prompt", &context)
|
||||||
|
}
|
||||||
|
}
|
1102
crates/assistant2/src/streaming_diff.rs
Normal file
1102
crates/assistant2/src/streaming_diff.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -406,7 +406,12 @@ fn main() {
|
||||||
stdout_is_a_pty(),
|
stdout_is_a_pty(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assistant2::init(cx);
|
assistant2::init(
|
||||||
|
app_state.fs.clone(),
|
||||||
|
app_state.client.clone(),
|
||||||
|
stdout_is_a_pty(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
assistant_tools::init(cx);
|
assistant_tools::init(cx);
|
||||||
repl::init(
|
repl::init(
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue