Compare commits
9 Commits
acp-rewind
...
v0.183.1-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83ce1712dc | ||
|
|
9a54d111ef | ||
|
|
c2ff375787 | ||
|
|
1a81946137 | ||
|
|
36ca5ab7c2 | ||
|
|
ad3a319465 | ||
|
|
19b7c1ae89 | ||
|
|
9f8320f3a3 | ||
|
|
7c483b231d |
115
Cargo.lock
generated
115
Cargo.lock
generated
@@ -324,7 +324,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"thiserror 2.0.12",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -567,7 +567,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -704,6 +704,7 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
@@ -721,9 +722,11 @@ dependencies = [
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"web_search",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1881,7 +1884,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
@@ -3028,7 +3031,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry_events",
|
||||
@@ -3360,7 +3363,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"task",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -4477,7 +4480,7 @@ dependencies = [
|
||||
"optfield",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
@@ -5122,7 +5125,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5973,7 +5976,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
@@ -6066,7 +6069,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -6172,7 +6175,7 @@ dependencies = [
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"thiserror 2.0.12",
|
||||
@@ -6820,7 +6823,7 @@ name = "icons"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -7088,7 +7091,7 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -7674,7 +7677,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
@@ -7734,7 +7737,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"theme",
|
||||
"thiserror 2.0.12",
|
||||
"tiktoken-rs",
|
||||
@@ -7742,6 +7745,7 @@ dependencies = [
|
||||
"ui",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8706,7 +8710,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -9553,7 +9557,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -12132,7 +12136,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"tracing",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -12660,7 +12664,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
@@ -13705,7 +13709,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"story",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"ui",
|
||||
@@ -13787,7 +13791,16 @@ version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.26.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13803,6 +13816,19 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -14418,7 +14444,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"serde_repr",
|
||||
"settings",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -14452,7 +14478,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"simplelog",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"theme",
|
||||
"vscode_theme",
|
||||
"workspace-hack",
|
||||
@@ -15453,7 +15479,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"theme",
|
||||
"ui_macros",
|
||||
"util",
|
||||
@@ -16586,6 +16612,36 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_search"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"gpui",
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_search_providers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language_model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"web_search",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "0.26.8"
|
||||
@@ -17624,7 +17680,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sqlez",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
@@ -17769,7 +17825,7 @@ dependencies = [
|
||||
"sqlx-macros-core",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"subtle",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.100",
|
||||
@@ -18141,7 +18197,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.183.0"
|
||||
version = "0.183.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -18264,6 +18320,8 @@ dependencies = [
|
||||
"uuid",
|
||||
"vim",
|
||||
"vim_mode_setting",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
"welcome",
|
||||
"windows 0.61.1",
|
||||
"winresource",
|
||||
@@ -18328,12 +18386,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf21350eced858d129840589158a8f6895c4fa4327ae56dd8c7d6a98495bed4"
|
||||
checksum = "57a5e1b5b3ace3fb55292a4c14036723bb8a01fac4aeaa3c2b63b51228412f94"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ members = [
|
||||
"crates/util_macros",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -370,6 +372,8 @@ util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
web_search_providers = { path = "crates/web_search_providers" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -601,7 +605,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.4"
|
||||
zed_llm_client = "0.5.0"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
|
||||
@@ -630,6 +630,7 @@
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-alt-p": "assistant::OpenPromptLibrary",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
|
||||
@@ -652,7 +652,8 @@
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"thinking": true
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
@@ -678,7 +679,8 @@
|
||||
"regex_search": true,
|
||||
"rename": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,11 +5,12 @@ use crate::thread::{
|
||||
ThreadEvent, ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
|
||||
@@ -943,8 +944,8 @@ impl ActiveThread {
|
||||
&tool_use.input,
|
||||
self.thread
|
||||
.read(cx)
|
||||
.tool_result(&tool_use.id)
|
||||
.map(|result| result.content.clone().into())
|
||||
.output_for_tool(&tool_use.id)
|
||||
.map(|output| output.clone().into())
|
||||
.unwrap_or("".into()),
|
||||
cx,
|
||||
);
|
||||
@@ -2279,12 +2280,15 @@ impl ActiveThread {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
return card.render(&tool_use.status, window, cx);
|
||||
}
|
||||
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let is_status_finished = matches!(&tool_use.status, ToolUseStatus::Finished(_));
|
||||
|
||||
let fs = self
|
||||
@@ -2343,6 +2347,9 @@ impl ActiveThread {
|
||||
rendered.input.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
@@ -2369,12 +2376,16 @@ impl ActiveThread {
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
)),
|
||||
),
|
||||
@@ -2431,6 +2442,7 @@ impl ActiveThread {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
})),
|
||||
),
|
||||
),
|
||||
@@ -2544,7 +2556,7 @@ impl ActiveThread {
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.my_3()
|
||||
.my_2()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -2761,7 +2773,7 @@ impl ActiveThread {
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}).into_any_element()
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
|
||||
@@ -25,6 +25,7 @@ use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use time::UtcOffset;
|
||||
use ui::{
|
||||
@@ -1112,16 +1113,16 @@ impl AssistantPanel {
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Settings", OpenConfiguration.boxed_clone())
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.action(
|
||||
"Install MCPs",
|
||||
zed_actions::Extensions {
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}
|
||||
.boxed_clone(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
@@ -1449,6 +1450,9 @@ impl AssistantPanel {
|
||||
ThreadError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
}
|
||||
ThreadError::ModelRequestLimitReached { plan } => {
|
||||
self.render_model_request_limit_reached_error(plan, cx)
|
||||
}
|
||||
ThreadError::Message { header, message } => {
|
||||
self.render_error_message(header, message, cx)
|
||||
}
|
||||
@@ -1551,6 +1555,67 @@ impl AssistantPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
&self,
|
||||
plan: Plan,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let error_message = match plan {
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
};
|
||||
let call_to_action = match plan {
|
||||
Plan::Free => "Upgrade to Zed Pro",
|
||||
Plan::ZedPro => "Upgrade to usage-based billing",
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", call_to_action).on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
&self,
|
||||
header: SharedString,
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
@@ -37,7 +38,24 @@ pub(crate) enum Match {
|
||||
File(FileMatch),
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Mode(ContextPickerMode),
|
||||
Mode(ModeMatch),
|
||||
}
|
||||
|
||||
pub struct ModeMatch {
|
||||
mat: Option<StringMatch>,
|
||||
mode: ContextPickerMode,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
pub fn score(&self) -> f64 {
|
||||
match self {
|
||||
Match::File(file) => file.mat.score,
|
||||
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
||||
Match::Thread(_) => 1.,
|
||||
Match::Symbol(_) => 1.,
|
||||
Match::Fetch(_) => 1.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
@@ -126,19 +144,54 @@ fn search(
|
||||
matches.extend(
|
||||
supported_context_picker_modes(&thread_store)
|
||||
.into_iter()
|
||||
.map(Match::Mode),
|
||||
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
|
||||
);
|
||||
|
||||
Task::ready(matches)
|
||||
} else {
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
|
||||
let modes = supported_context_picker_modes(&thread_store);
|
||||
let mode_candidates = modes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
search_files_task
|
||||
let mut matches = search_files_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::File)
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mode_matches = fuzzy::match_strings(
|
||||
&mode_candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Arc::new(AtomicBool::default()),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches.extend(mode_matches.into_iter().map(|mat| {
|
||||
Match::Mode(ModeMatch {
|
||||
mode: modes[mat.candidate_id],
|
||||
mat: Some(mat),
|
||||
})
|
||||
}));
|
||||
|
||||
matches.sort_by(|a, b| {
|
||||
b.score()
|
||||
.partial_cmp(&a.score())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
matches
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -548,7 +601,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
)),
|
||||
Match::Mode(mode) => {
|
||||
Match::Mode(ModeMatch { mode, .. }) => {
|
||||
Some(Self::completion_for_mode(source_range.clone(), mode))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
@@ -18,12 +18,13 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, TokenUsage,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, Role, StopReason, TokenUsage,
|
||||
};
|
||||
use project::Project;
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -630,6 +631,14 @@ impl Thread {
|
||||
self.tool_use.tool_result(id)
|
||||
}
|
||||
|
||||
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
|
||||
Some(&self.tool_use.tool_result(id)?.content)
|
||||
}
|
||||
|
||||
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
|
||||
self.tool_use.tool_result_card(id).cloned()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
@@ -1150,6 +1159,12 @@ impl Thread {
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::MaxMonthlySpendReached,
|
||||
));
|
||||
} else if let Some(error) =
|
||||
error.downcast_ref::<ModelRequestLimitReachedError>()
|
||||
{
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::ModelRequestLimitReached { plan: error.plan },
|
||||
));
|
||||
} else if let Some(known_error) =
|
||||
error.downcast_ref::<LanguageModelKnownError>()
|
||||
{
|
||||
@@ -1419,6 +1434,12 @@ impl Thread {
|
||||
)
|
||||
};
|
||||
|
||||
// Store the card separately if it exists
|
||||
if let Some(card) = tool_result.card.clone() {
|
||||
self.tool_use
|
||||
.insert_tool_result_card(tool_use_id.clone(), card);
|
||||
}
|
||||
|
||||
cx.spawn({
|
||||
async move |thread: WeakEntity<Thread>, cx| {
|
||||
let output = tool_result.output.await;
|
||||
@@ -1929,6 +1950,8 @@ pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
#[error("Max monthly spend reached")]
|
||||
MaxMonthlySpendReached,
|
||||
#[error("Model request limit reached")]
|
||||
ModelRequestLimitReached { plan: Plan },
|
||||
#[error("Message {header}: {message}")]
|
||||
Message {
|
||||
header: SharedString,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{Tool, ToolWorkingSet};
|
||||
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
@@ -27,26 +27,7 @@ pub struct ToolUse {
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
@@ -54,10 +35,9 @@ pub struct ToolUseState {
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
|
||||
Self {
|
||||
@@ -66,6 +46,7 @@ impl ToolUseState {
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +238,18 @@ impl ToolUseState {
|
||||
self.tool_results.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
|
||||
self.tool_result_cards.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn insert_tool_result_card(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
card: AnyToolCard,
|
||||
) {
|
||||
self.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
|
||||
pub fn request_tool_use(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
|
||||
@@ -191,15 +191,12 @@ impl RenderOnce for ContextPill {
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
icon_path: _,
|
||||
kind,
|
||||
kind: _,
|
||||
focused,
|
||||
on_click,
|
||||
} => base_pill
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.border_dashed()
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
@@ -207,30 +204,17 @@ impl RenderOnce for ContextPill {
|
||||
color.border
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.child(
|
||||
div().px_0p5().max_w_64().child(
|
||||
div().max_w_64().child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread
|
||||
| ContextKind::Directory
|
||||
| ContextKind::FetchedUrl
|
||||
| ContextKind::Symbol => "Active",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::XSmall)
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
|
||||
})
|
||||
|
||||
@@ -9,6 +9,10 @@ use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::Context;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
@@ -24,16 +28,87 @@ pub fn init(cx: &mut App) {
|
||||
ToolRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
/// The result of running a tool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of running a tool, containing both the asynchronous output
|
||||
/// and an optional card view that can be rendered immediately.
|
||||
pub struct ToolResult {
|
||||
/// The asynchronous task that will eventually resolve to the tool's output
|
||||
pub output: Task<Result<String>>,
|
||||
/// An optional view to present the output of the tool.
|
||||
pub card: Option<AnyToolCard>,
|
||||
}
|
||||
|
||||
pub trait ToolCard: 'static + Sized {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
render: fn(
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement,
|
||||
}
|
||||
|
||||
impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
fn from(entity: Entity<T>) -> Self {
|
||||
fn downcast_render<T: ToolCard>(
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let entity = entity.downcast::<T>().unwrap();
|
||||
entity.update(cx, |entity, cx| {
|
||||
entity.render(status, window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
Self {
|
||||
entity: entity.into(),
|
||||
render: downcast_render::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyToolCard {
|
||||
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Task<Result<String>>> for ToolResult {
|
||||
/// Convert from a task to a ToolResult
|
||||
/// Convert from a task to a ToolResult with no card
|
||||
fn from(output: Task<Result<String>>) -> Self {
|
||||
Self { output }
|
||||
Self { output, card: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -32,7 +33,9 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -22,14 +22,17 @@ mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
use crate::batch_tool::BatchTool;
|
||||
use crate::code_action_tool::CodeActionTool;
|
||||
@@ -56,28 +59,39 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(BatchTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
213
crates/assistant_tools/src/web_search_tool.rs
Normal file
213
crates/assistant_tools/src/web_search_tool.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
/// The search term or question to query on the web.
|
||||
query: String,
|
||||
}
|
||||
|
||||
pub struct WebSearchTool;
|
||||
|
||||
impl Tool for WebSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"web_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Globe
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<WebSearchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Web Search".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
|
||||
return Task::ready(Err(anyhow!("Web search is not available."))).into();
|
||||
};
|
||||
|
||||
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
|
||||
let output = cx.background_spawn({
|
||||
let search_task = search_task.clone();
|
||||
async move {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err))?;
|
||||
serde_json::to_string(&response).context("Failed to serialize search results")
|
||||
}
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebSearchToolCard {
|
||||
response: Option<Result<WebSearchResponse>>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl WebSearchToolCard {
|
||||
fn new(
|
||||
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn(async move |this, cx| {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err));
|
||||
this.update(cx, |this, cx| {
|
||||
this.response = Some(response);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Self {
|
||||
response: None,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for WebSearchToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
_status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let header = h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(IconName::Globe)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(match self.response.as_ref() {
|
||||
Some(Ok(response)) => {
|
||||
let text: SharedString = if response.citations.len() == 1 {
|
||||
"1 result".into()
|
||||
} else {
|
||||
format!("{} results", response.citations.len()).into()
|
||||
};
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Label::new("Searched the Web").size(LabelSize::Small))
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text),
|
||||
)
|
||||
.child(Label::new(text).size(LabelSize::Small))
|
||||
.into_any_element()
|
||||
}
|
||||
Some(Err(error)) => div()
|
||||
.id("web-search-error")
|
||||
.child(Label::new("Web Search failed").size(LabelSize::Small))
|
||||
.tooltip(Tooltip::text(error.to_string()))
|
||||
.into_any_element(),
|
||||
|
||||
None => Label::new("Searching the Web…")
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"web-search-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
.into_any();
|
||||
|
||||
let content =
|
||||
self.response.as_ref().and_then(|response| match response {
|
||||
Ok(response) => {
|
||||
Some(
|
||||
v_flex()
|
||||
.ml_1p5()
|
||||
.pl_1p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.gap_1()
|
||||
.children(response.citations.iter().enumerate().map(
|
||||
|(index, citation)| {
|
||||
let title = citation.title.clone();
|
||||
let url = citation.url.clone();
|
||||
|
||||
Button::new(("citation", index), title)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.truncate(true)
|
||||
.tooltip({
|
||||
let url = url.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Citation Link",
|
||||
None,
|
||||
url.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
move |_, _, cx| cx.open_url(&url)
|
||||
})
|
||||
},
|
||||
))
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
Err(_) => None,
|
||||
});
|
||||
|
||||
v_flex().my_2().gap_1().child(header).children(content)
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,8 @@ use language::{
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
|
||||
RowInfo, ToOffset, ToPoint,
|
||||
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@@ -574,6 +574,21 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
|
||||
let to_remove = self
|
||||
.inlay_map
|
||||
.current_inlays()
|
||||
.filter_map(|inlay| {
|
||||
if excerpts_removed.contains(&inlay.position.excerpt_id) {
|
||||
Some(inlay.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.inlay_map.splice(&to_remove, Vec::new());
|
||||
}
|
||||
|
||||
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
|
||||
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
|
||||
let language = buffer
|
||||
|
||||
@@ -4170,10 +4170,13 @@ impl Editor {
|
||||
if let Some(InlaySplice {
|
||||
to_remove,
|
||||
to_insert,
|
||||
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
|
||||
}) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
|
||||
{
|
||||
self.splice_inlays(&to_remove, to_insert, cx);
|
||||
}
|
||||
self.display_map.update(cx, |display_map, _| {
|
||||
display_map.remove_inlays_for_excerpts(&excerpts_removed)
|
||||
});
|
||||
return;
|
||||
}
|
||||
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
|
||||
@@ -555,12 +555,12 @@ impl InlayHintCache {
|
||||
/// Completely forget of certain excerpts that were removed from the multibuffer.
|
||||
pub(super) fn remove_excerpts(
|
||||
&mut self,
|
||||
excerpts_removed: Vec<ExcerptId>,
|
||||
excerpts_removed: &[ExcerptId],
|
||||
) -> Option<InlaySplice> {
|
||||
let mut to_remove = Vec::new();
|
||||
for excerpt_to_remove in excerpts_removed {
|
||||
self.update_tasks.remove(&excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
|
||||
self.update_tasks.remove(excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
|
||||
let cached_hints = cached_hints.read();
|
||||
to_remove.extend(cached_hints.ordered_hints.iter().copied());
|
||||
}
|
||||
|
||||
@@ -84,6 +84,11 @@ impl FeatureFlag for ZedPro {
|
||||
const NAME: &'static str = "zed-pro";
|
||||
}
|
||||
|
||||
pub struct ZedProWebSearchTool {}
|
||||
impl FeatureFlag for ZedProWebSearchTool {
|
||||
const NAME: &'static str = "zed-pro-web-search-tool";
|
||||
}
|
||||
|
||||
pub struct NotebookFeatureFlag;
|
||||
|
||||
impl FeatureFlag for NotebookFeatureFlag {
|
||||
|
||||
@@ -142,6 +142,24 @@ impl fmt::Display for MaxMonthlySpendReachedError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub struct ModelRequestLimitReachedError {
|
||||
pub plan: Plan,
|
||||
}
|
||||
|
||||
impl fmt::Display for ModelRequestLimitReachedError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let message = match self.plan {
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
};
|
||||
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -16,18 +16,21 @@ use language_model::{
|
||||
AuthenticateError, CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
|
||||
LanguageModelKnownError, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
|
||||
LanguageModelToolSchemaFormat, RateLimiter, ZED_CLOUD_PROVIDER_ID,
|
||||
LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
|
||||
MaxMonthlySpendReachedError, PaymentRequiredError, RefreshLlmTokenListener,
|
||||
};
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::value::RawValue;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::Timer;
|
||||
use smol::io::{AsyncReadExt, BufReader};
|
||||
use std::str::FromStr as _;
|
||||
use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
@@ -35,6 +38,7 @@ use std::{
|
||||
use strum::IntoEnumIterator;
|
||||
use thiserror::Error;
|
||||
use ui::{TintColor, prelude::*};
|
||||
use zed_llm_client::{CURRENT_PLAN_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME};
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::provider::anthropic::{count_anthropic_tokens, into_anthropic};
|
||||
@@ -551,6 +555,32 @@ impl CloudLanguageModel {
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!(MaxMonthlySpendReachedError));
|
||||
} else if status == StatusCode::FORBIDDEN
|
||||
&& response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
if let Some("model_requests") = response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
.and_then(|resource| resource.to_str().ok())
|
||||
{
|
||||
if let Some(plan) = response
|
||||
.headers()
|
||||
.get(CURRENT_PLAN_HEADER_NAME)
|
||||
.and_then(|plan| plan.to_str().ok())
|
||||
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
|
||||
{
|
||||
let plan = match plan {
|
||||
zed_llm_client::Plan::Free => Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
};
|
||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(anyhow!("Forbidden"));
|
||||
} else if status.as_u16() >= 500 && status.as_u16() < 600 {
|
||||
// If we encounter an error in the 500 range, retry after a delay.
|
||||
// We've seen at least these in the wild from API providers:
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Anchor {
|
||||
if self_excerpt_id == ExcerptId::min() || self_excerpt_id == ExcerptId::max() {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) {
|
||||
let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer);
|
||||
if text_cmp.is_ne() {
|
||||
return text_cmp;
|
||||
|
||||
@@ -6041,7 +6041,7 @@ impl MultiBufferSnapshot {
|
||||
return &entry.locator;
|
||||
}
|
||||
}
|
||||
panic!("invalid excerpt id {:?}", id)
|
||||
panic!("invalid excerpt id {id:?}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ use ui::{
|
||||
IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
|
||||
|
||||
pub use onboarding_banner::restore_banner;
|
||||
@@ -210,7 +210,6 @@ impl Render for TitleBar {
|
||||
.pr_1()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.children(self.render_call_controls(window, cx))
|
||||
.child(self.render_bottom_dock_layout_menu(cx))
|
||||
.map(|el| {
|
||||
let status = self.client.status();
|
||||
let status = &*status.borrow();
|
||||
@@ -623,101 +622,6 @@ impl TitleBar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_bottom_dock_layout_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let workspace = self.workspace.upgrade().unwrap();
|
||||
let current_layout = workspace.update(cx, |workspace, _cx| workspace.bottom_dock_layout());
|
||||
|
||||
PopoverMenu::new("layout-menu")
|
||||
.trigger(
|
||||
IconButton::new("toggle_layout", IconName::Layout)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Toggle Layout Menu")),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu(move |window, cx| {
|
||||
ContextMenu::build(window, cx, {
|
||||
let workspace = workspace.clone();
|
||||
move |menu, _, _| {
|
||||
menu.label("Bottom Dock")
|
||||
.separator()
|
||||
.toggleable_entry(
|
||||
"Contained",
|
||||
current_layout == BottomDockLayout::Contained,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::Contained,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Full",
|
||||
current_layout == BottomDockLayout::Full,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::Full,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Left Aligned",
|
||||
current_layout == BottomDockLayout::LeftAligned,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::LeftAligned,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Right Aligned",
|
||||
current_layout == BottomDockLayout::RightAligned,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::RightAligned,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
|
||||
let client = self.client.clone();
|
||||
Button::new("sign_in", "Sign in")
|
||||
|
||||
@@ -160,7 +160,11 @@ impl Render for Tooltip {
|
||||
}),
|
||||
)
|
||||
.when_some(self.meta.clone(), |this, meta| {
|
||||
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
|
||||
this.child(
|
||||
div()
|
||||
.max_w_72()
|
||||
.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
20
crates/web_search/Cargo.toml
Normal file
20
crates/web_search/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "web_search"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/web_search.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
1
crates/web_search/LICENSE-GPL
Symbolic link
1
crates/web_search/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
64
crates/web_search/src/web_search.rs
Normal file
64
crates/web_search/src/web_search.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task};
|
||||
use std::sync::Arc;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| WebSearchRegistry::default());
|
||||
cx.set_global(GlobalWebSearchRegistry(registry));
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
|
||||
pub struct WebSearchProviderId(pub SharedString);
|
||||
|
||||
pub trait WebSearchProvider {
|
||||
fn id(&self) -> WebSearchProviderId;
|
||||
fn search(&self, query: String, cx: &mut App) -> Task<Result<WebSearchResponse>>;
|
||||
}
|
||||
|
||||
struct GlobalWebSearchRegistry(Entity<WebSearchRegistry>);
|
||||
|
||||
impl Global for GlobalWebSearchRegistry {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WebSearchRegistry {
|
||||
providers: HashMap<WebSearchProviderId, Arc<dyn WebSearchProvider>>,
|
||||
active_provider: Option<Arc<dyn WebSearchProvider>>,
|
||||
}
|
||||
|
||||
impl WebSearchRegistry {
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalWebSearchRegistry>().0.clone()
|
||||
}
|
||||
|
||||
pub fn read_global(cx: &App) -> &Self {
|
||||
cx.global::<GlobalWebSearchRegistry>().0.read(cx)
|
||||
}
|
||||
|
||||
pub fn providers(&self) -> impl Iterator<Item = &Arc<dyn WebSearchProvider>> {
|
||||
self.providers.values()
|
||||
}
|
||||
|
||||
pub fn active_provider(&self) -> Option<Arc<dyn WebSearchProvider>> {
|
||||
self.active_provider.clone()
|
||||
}
|
||||
|
||||
pub fn set_active_provider(&mut self, provider: Arc<dyn WebSearchProvider>) {
|
||||
self.active_provider = Some(provider.clone());
|
||||
self.providers.insert(provider.id(), provider);
|
||||
}
|
||||
|
||||
pub fn register_provider<T: WebSearchProvider + 'static>(
|
||||
&mut self,
|
||||
provider: T,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = provider.id();
|
||||
let provider = Arc::new(provider);
|
||||
self.providers.insert(id.clone(), provider.clone());
|
||||
if self.active_provider.is_none() {
|
||||
self.active_provider = Some(provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
crates/web_search_providers/Cargo.toml
Normal file
26
crates/web_search_providers/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "web_search_providers"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/web_search_providers.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language_model.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
1
crates/web_search_providers/LICENSE-GPL
Symbolic link
1
crates/web_search_providers/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
103
crates/web_search_providers/src/cloud.rs
Normal file
103
crates/web_search_providers/src/cloud.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use client::Client;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, AppContext, Context, Entity, Subscription, Task};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language_model::{LlmApiToken, RefreshLlmTokenListener};
|
||||
use web_search::{WebSearchProvider, WebSearchProviderId};
|
||||
use zed_llm_client::{WebSearchBody, WebSearchResponse};
|
||||
|
||||
pub struct CloudWebSearchProvider {
|
||||
state: Entity<State>,
|
||||
}
|
||||
|
||||
impl CloudWebSearchProvider {
|
||||
pub fn new(client: Arc<Client>, cx: &mut App) -> Self {
|
||||
let state = cx.new(|cx| State::new(client, cx));
|
||||
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
_llm_token_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(client: Arc<Client>, cx: &mut Context<Self>) -> Self {
|
||||
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
|
||||
|
||||
Self {
|
||||
client,
|
||||
llm_api_token: LlmApiToken::default(),
|
||||
_llm_token_subscription: cx.subscribe(
|
||||
&refresh_llm_token_listener,
|
||||
|this, _, _event, cx| {
|
||||
let client = this.client.clone();
|
||||
let llm_api_token = this.llm_api_token.clone();
|
||||
cx.spawn(async move |_this, _cx| {
|
||||
llm_api_token.refresh(&client).await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSearchProvider for CloudWebSearchProvider {
|
||||
fn id(&self) -> WebSearchProviderId {
|
||||
WebSearchProviderId("zed.dev".into())
|
||||
}
|
||||
|
||||
fn search(&self, query: String, cx: &mut App) -> Task<Result<WebSearchResponse>> {
|
||||
let state = self.state.read(cx);
|
||||
let client = state.client.clone();
|
||||
let llm_api_token = state.llm_api_token.clone();
|
||||
let body = WebSearchBody { query };
|
||||
cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await })
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_web_search(
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
body: WebSearchBody,
|
||||
) -> Result<WebSearchResponse> {
|
||||
let http_client = &client.http_client();
|
||||
|
||||
let token = llm_api_token.acquire(&client).await?;
|
||||
|
||||
let request_builder = http_client::Request::builder().method(Method::POST);
|
||||
let request_builder = if let Ok(web_search_url) = std::env::var("ZED_WEB_SEARCH_URL") {
|
||||
request_builder.uri(web_search_url)
|
||||
} else {
|
||||
request_builder.uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref())
|
||||
};
|
||||
let request = request_builder
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {token}"))
|
||||
.body(serde_json::to_string(&body)?.into())?;
|
||||
let mut response = http_client
|
||||
.send(request)
|
||||
.await
|
||||
.context("failed to send web search request")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Ok(serde_json::from_str(&body)?);
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
return Err(anyhow!(
|
||||
"error performing web search.\nStatus: {:?}\nBody: {body}",
|
||||
response.status(),
|
||||
));
|
||||
}
|
||||
}
|
||||
35
crates/web_search_providers/src/web_search_providers.rs
Normal file
35
crates/web_search_providers/src/web_search_providers.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
mod cloud;
|
||||
|
||||
use client::Client;
|
||||
use feature_flags::{FeatureFlagAppExt, ZedProWebSearchTool};
|
||||
use gpui::{App, Context};
|
||||
use std::sync::Arc;
|
||||
use web_search::WebSearchRegistry;
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut App) {
|
||||
let registry = WebSearchRegistry::global(cx);
|
||||
registry.update(cx, |registry, cx| {
|
||||
register_web_search_providers(registry, client, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_web_search_providers(
|
||||
_registry: &mut WebSearchRegistry,
|
||||
client: Arc<Client>,
|
||||
cx: &mut Context<WebSearchRegistry>,
|
||||
) {
|
||||
cx.observe_flag::<ZedProWebSearchTool, _>({
|
||||
let client = client.clone();
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
WebSearchRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.register_provider(
|
||||
cloud::CloudWebSearchProvider::new(client.clone(), cx),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.183.0"
|
||||
version = "0.183.1"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
@@ -133,6 +133,8 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
vim.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
web_search.workspace = true
|
||||
web_search_providers.workspace = true
|
||||
welcome.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -490,6 +490,8 @@ fn main() {
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
web_search::init(cx);
|
||||
web_search_providers::init(app_state.client.clone(), cx);
|
||||
snippet_provider::init(cx);
|
||||
inline_completion_registry::init(
|
||||
app_state.client.clone(),
|
||||
|
||||
@@ -4258,6 +4258,8 @@ mod tests {
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
web_search::init(cx);
|
||||
web_search_providers::init(app_state.client.clone(), cx);
|
||||
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
|
||||
assistant::init(
|
||||
app_state.fs.clone(),
|
||||
|
||||
@@ -75,6 +75,46 @@ Non-negative `float` values
|
||||
|
||||
`float` values
|
||||
|
||||
## Bottom Dock Layout
|
||||
|
||||
- Description: Control the layout of the bottom dock, relative to the left and right docks
|
||||
- Setting: `bottom_dock_layout`
|
||||
- Default: `"contained"`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Contain the bottom dock, giving the full height of the window to the left and right docks
|
||||
|
||||
```json
|
||||
{
|
||||
"bottom_dock_layout": "contained"
|
||||
}
|
||||
```
|
||||
|
||||
2. Give the bottom dock the full width of the window, truncating the left and right docks
|
||||
|
||||
```json
|
||||
{
|
||||
"bottom_dock_layout": "full"
|
||||
}
|
||||
```
|
||||
|
||||
3. Left align the bottom dock, truncating the left dock and giving the right dock the full height of the window
|
||||
|
||||
```json
|
||||
{
|
||||
"bottom_dock_layout": "left_aligned"
|
||||
}
|
||||
```
|
||||
|
||||
3. Right align the bottom dock, giving the left dock the full height of the window and truncating the right dock.
|
||||
|
||||
```json
|
||||
{
|
||||
"bottom_dock_layout": "right_aligned"
|
||||
}
|
||||
```
|
||||
|
||||
## Auto Install extensions
|
||||
|
||||
- Description: Define extensions to be autoinstalled or never be installed.
|
||||
|
||||
Reference in New Issue
Block a user