Compare commits
1 Commits
add-walkth
...
direct-lin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4727aad8f8 |
83
Cargo.lock
generated
83
Cargo.lock
generated
@@ -56,7 +56,6 @@ dependencies = [
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_tool",
|
||||
"async-watch",
|
||||
"buffer_diff",
|
||||
@@ -79,7 +78,6 @@ dependencies = [
|
||||
"heed",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"indexed_docs",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"jsonschema",
|
||||
@@ -472,6 +470,68 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
"assistant_slash_command",
|
||||
"assistant_slash_commands",
|
||||
"assistant_tool",
|
||||
"async-watch",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"ctor",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indexed_docs",
|
||||
"indoc",
|
||||
"language",
|
||||
"language_model",
|
||||
"language_model_selector",
|
||||
"languages",
|
||||
"log",
|
||||
"lsp",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rope",
|
||||
"rules_library",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"smol",
|
||||
"streaming_diff",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter-md",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant_context_editor"
|
||||
version = "0.1.0"
|
||||
@@ -2942,6 +3002,7 @@ name = "collab"
|
||||
version = "0.44.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant",
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
"assistant_slash_command",
|
||||
@@ -4007,6 +4068,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"language",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
@@ -4042,6 +4104,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp-types",
|
||||
"paths",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4186,7 +4249,6 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"editor",
|
||||
@@ -7735,7 +7797,6 @@ dependencies = [
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
@@ -7843,7 +7904,6 @@ dependencies = [
|
||||
"partial-json-fixer",
|
||||
"project",
|
||||
"proto",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -17034,26 +17094,18 @@ dependencies = [
|
||||
"db",
|
||||
"documented",
|
||||
"editor",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"install_cli",
|
||||
"language",
|
||||
"language_model",
|
||||
"linkme",
|
||||
"paths",
|
||||
"picker",
|
||||
"project",
|
||||
"regex",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
"time_format",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
@@ -18649,6 +18701,7 @@ dependencies = [
|
||||
"ashpd",
|
||||
"askpass",
|
||||
"assets",
|
||||
"assistant",
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
"assistant_tools",
|
||||
@@ -18830,9 +18883,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.0"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23b2fd00776b0c55072f389654910ceb501eb0083d7f78905ab0e5cc86949ec"
|
||||
checksum = "6fe0d60001c02d0d21a4114a13bee3a905fbb9e146ada80a90435c05fda18852"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -6,6 +6,7 @@ members = [
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant_context_editor",
|
||||
"crates/assistant_settings",
|
||||
"crates/assistant_slash_command",
|
||||
@@ -213,6 +214,7 @@ ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant_context_editor = { path = "crates/assistant_context_editor" }
|
||||
assistant_settings = { path = "crates/assistant_settings" }
|
||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||
@@ -608,7 +610,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.0"
|
||||
zed_llm_client = "0.7.5"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 412 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<path d="M18.905 12.75a1.25 1.25 0 1 1-2.5 0v-7.5a1.25 1.25 0 0 1 2.5 0v7.5ZM8.905 17v1.3c0 .268-.14.526-.395.607A2 2 0 0 1 5.905 17c0-.995.182-1.948.514-2.826.204-.54-.166-1.174-.744-1.174h-2.52c-1.243 0-2.261-1.01-2.146-2.247.193-2.08.651-4.082 1.341-5.974C2.752 3.678 3.833 3 5.005 3h3.192a3 3 0 0 1 1.341.317l2.734 1.366A3 3 0 0 0 13.613 5h1.292v7h-.963c-.685 0-1.258.482-1.612 1.068a4.01 4.01 0 0 1-2.166 1.73c-.432.143-.853.386-1.011.814-.16.432-.248.9-.248 1.388Z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 580 B After Width: | Height: | Size: 405 B |
@@ -1,3 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
|
||||
<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 404 B |
@@ -213,6 +213,21 @@
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"bindings": {
|
||||
"ctrl-k c": "assistant::CopyCode",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"ctrl-k h": "assistant::DeployHistory",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary",
|
||||
"new": "assistant::NewChat",
|
||||
"ctrl-t": "assistant::NewChat",
|
||||
"ctrl-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
@@ -698,8 +713,8 @@
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -258,6 +258,21 @@
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k c": "assistant::CopyCode",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPreviousMatch",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"cmd-k h": "assistant::DeployHistory",
|
||||
"cmd-k l": "agent::OpenRulesLibrary",
|
||||
"cmd-t": "assistant::NewChat",
|
||||
"cmd-n": "assistant::NewChat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -765,8 +780,8 @@
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist"
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -356,49 +356,6 @@
|
||||
"vertical": true
|
||||
}
|
||||
},
|
||||
// Minimap related settings
|
||||
"minimap": {
|
||||
// When to show the minimap in the editor.
|
||||
// This setting can take three values:
|
||||
// 1. Show the minimap if the editor's scrollbar is visible:
|
||||
// "auto"
|
||||
// 2. Always show the minimap:
|
||||
// "always"
|
||||
// 3. Never show the minimap:
|
||||
// "never" (default)
|
||||
"show": "never",
|
||||
// When to show the minimap thumb.
|
||||
// This setting can take two values:
|
||||
// 1. Show the minimap thumb if the mouse is over the minimap:
|
||||
// "hover"
|
||||
// 2. Always show the minimap thumb:
|
||||
// "always" (default)
|
||||
"thumb": "always",
|
||||
// How the minimap thumb border should look.
|
||||
// This setting can take five values:
|
||||
// 1. Display a border on all sides of the thumb:
|
||||
// "thumb_border": "full"
|
||||
// 2. Display a border on all sides except the left side of the thumb:
|
||||
// "thumb_border": "left_open" (default)
|
||||
// 3. Display a border on all sides except the right side of the thumb:
|
||||
// "thumb_border": "right_open"
|
||||
// 4. Display a border only on the left side of the thumb:
|
||||
// "thumb_border": "left_only"
|
||||
// 5. Display the thumb without any border:
|
||||
// "thumb_border": "none"
|
||||
"thumb_border": "left_open",
|
||||
// How to highlight the current line in the minimap.
|
||||
// This setting can take the following values:
|
||||
//
|
||||
// 1. `null` to inherit the editor `current_line_highlight` setting (default)
|
||||
// 2. "line" or "all" to highlight the current line in the minimap.
|
||||
// 3. "gutter" or "none" to not highlight the current line in the minimap.
|
||||
"current_line_highlight": null,
|
||||
// The width of the minimap in pixels.
|
||||
"width": 100,
|
||||
// The font size of the minimap in pixels.
|
||||
"font_size": 2
|
||||
},
|
||||
// Enable middle-click paste on Linux.
|
||||
"middle_click_paste": true,
|
||||
// What to do when multibuffer is double clicked in some of its excerpts
|
||||
@@ -413,6 +370,8 @@
|
||||
"gutter": {
|
||||
// Whether to show line numbers in the gutter.
|
||||
"line_numbers": true,
|
||||
// Whether to show code action buttons in the gutter.
|
||||
"code_actions": true,
|
||||
// Whether to show runnables buttons in the gutter.
|
||||
"runnables": true,
|
||||
// Whether to show breakpoints in the gutter.
|
||||
|
||||
@@ -43,7 +43,6 @@ pub struct ActivityIndicator {
|
||||
context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ServerStatus {
|
||||
name: SharedString,
|
||||
status: BinaryStatus,
|
||||
@@ -71,7 +70,6 @@ impl ActivityIndicator {
|
||||
) -> Entity<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let workspace_handle = cx.entity();
|
||||
let this = cx.new(|cx| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -86,25 +84,6 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe_in(
|
||||
&workspace_handle,
|
||||
window,
|
||||
|activity_indicator, _, event, window, cx| match event {
|
||||
workspace::Event::ClearActivityIndicator { .. } => {
|
||||
if activity_indicator.statuses.pop().is_some() {
|
||||
activity_indicator.dismiss_error_message(
|
||||
&DismissErrorMessage,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|_, _, event, cx| match event {
|
||||
@@ -136,7 +115,7 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
Self {
|
||||
statuses: Vec::new(),
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
context_menu_handle: Default::default(),
|
||||
@@ -206,8 +185,11 @@ impl ActivityIndicator {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss_error(cx));
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
|
||||
@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/agent.rs"
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
@@ -23,7 +23,6 @@ anyhow.workspace = true
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
@@ -46,7 +45,6 @@ gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::AgentPanel;
|
||||
use crate::AssistantPanel;
|
||||
use crate::context::{AgentContextHandle, RULES_ICON};
|
||||
use crate::context_picker::{ContextPicker, MentionLink};
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -44,7 +44,8 @@ use std::time::Duration;
|
||||
use text::ToPoint;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, scrollbar, Disclosure, IconButton, KeyBinding, PopoverMenuHandle, ScrollbarState, TextSize, Tooltip
|
||||
Disclosure, IconButton, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
@@ -178,8 +179,8 @@ fn parse_markdown(
|
||||
pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems();
|
||||
let buffer_font_size = TextSize::Small.rems();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = window.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
@@ -275,8 +276,8 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
|
||||
fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let colors = cx.theme().colors();
|
||||
let ui_font_size = TextSize::Default.rems();
|
||||
let buffer_font_size = TextSize::Small.rems();
|
||||
let ui_font_size = TextSize::Default.rems(cx);
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
let mut text_style = window.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
@@ -312,7 +313,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
|
||||
font_family: Some(theme_settings.buffer_font.family.clone()),
|
||||
font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.buffer_font.features.clone()),
|
||||
font_size: Some(TextSize::XSmall.rems().into()),
|
||||
font_size: Some(TextSize::XSmall.rems(cx).into()),
|
||||
..Default::default()
|
||||
},
|
||||
heading: StyleRefinement {
|
||||
@@ -711,7 +712,7 @@ fn open_markdown_link(
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_thread_by_id(&thread_id, window, cx)
|
||||
@@ -720,7 +721,7 @@ fn open_markdown_link(
|
||||
}
|
||||
}),
|
||||
Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.open_saved_prompt_editor(path, window, cx)
|
||||
@@ -1210,7 +1211,8 @@ impl ActiveThread {
|
||||
|
||||
if let Some(workspace) = workspace_handle.upgrade() {
|
||||
workspace.update(_cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
workspace
|
||||
.focus_panel::<AssistantPanel>(window, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -1697,7 +1699,7 @@ impl ActiveThread {
|
||||
) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
.rems()
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx));
|
||||
let line_height = font_size * 1.75;
|
||||
|
||||
@@ -1778,7 +1780,8 @@ impl ActiveThread {
|
||||
let colors = cx.theme().colors();
|
||||
let editor_bg_color = colors.editor_background;
|
||||
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
@@ -1803,16 +1806,13 @@ impl ActiveThread {
|
||||
.mt_1()
|
||||
.py_2()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.mr_1()
|
||||
.opacity(0.4)
|
||||
.hover(|style| style.opacity(1.))
|
||||
.gap_1p5()
|
||||
.gap_1()
|
||||
.flex_wrap()
|
||||
.justify_end();
|
||||
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
|
||||
Some(feedback) => feedback_container
|
||||
.child(
|
||||
div().visible_on_hover("feedback_container").child(
|
||||
div().mr_1().visible_on_hover("feedback_container").child(
|
||||
Label::new(match feedback {
|
||||
ThreadFeedback::Positive => "Thanks for your feedback!",
|
||||
ThreadFeedback::Negative => {
|
||||
@@ -1825,8 +1825,11 @@ impl ActiveThread {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(match feedback {
|
||||
ThreadFeedback::Positive => Color::Accent,
|
||||
@@ -1844,6 +1847,7 @@ impl ActiveThread {
|
||||
)
|
||||
.child(
|
||||
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(match feedback {
|
||||
ThreadFeedback::Positive => Color::Ignored,
|
||||
@@ -1864,7 +1868,7 @@ impl ActiveThread {
|
||||
.into_any_element(),
|
||||
None => feedback_container
|
||||
.child(
|
||||
div().visible_on_hover("feedback_container").child(
|
||||
div().mr_1().visible_on_hover("feedback_container").child(
|
||||
Label::new(
|
||||
"Rating the thread sends all of your current conversation to the Zed team.",
|
||||
)
|
||||
@@ -1874,10 +1878,13 @@ impl ActiveThread {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Helpful Response"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
@@ -1892,6 +1899,7 @@ impl ActiveThread {
|
||||
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text("Not Helpful"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.handle_feedback_click(
|
||||
@@ -2283,7 +2291,7 @@ impl ActiveThread {
|
||||
let is_user_message = message_role == Role::User;
|
||||
|
||||
v_flex()
|
||||
.text_ui()
|
||||
.text_ui(cx)
|
||||
.gap_2()
|
||||
.when(is_user_message, |this| this.text_xs())
|
||||
.children(
|
||||
@@ -2312,7 +2320,7 @@ impl ActiveThread {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let buffer_font = theme_settings.buffer_font.family.clone();
|
||||
let buffer_font_size = TextSize::Small.rems();
|
||||
let buffer_font_size = TextSize::Small.rems(cx);
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(buffer_font),
|
||||
@@ -2524,7 +2532,7 @@ impl ActiveThread {
|
||||
.id(("thinking-content", ix))
|
||||
.max_h_20()
|
||||
.track_scroll(scroll_handle)
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
@@ -2553,7 +2561,7 @@ impl ActiveThread {
|
||||
.id(("thinking-content", ix))
|
||||
.h_full()
|
||||
.bg(editor_bg)
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
markdown.clone(),
|
||||
@@ -2615,7 +2623,7 @@ impl ActiveThread {
|
||||
.pl_2p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.when(is_open, |this| {
|
||||
this.child(
|
||||
MarkdownElement::new(
|
||||
@@ -2693,19 +2701,21 @@ impl ActiveThread {
|
||||
let rendered_tool_use = self.rendered_tool_uses.get(&tool_use.id).cloned();
|
||||
let results_content_container = || v_flex().p_2().gap_0p5();
|
||||
|
||||
let results_content =
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
results_content_container()
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(div().w_full().text_ui_sm().children(
|
||||
rendered_tool_use.as_ref().map(|rendered| {
|
||||
let results_content = v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
results_content_container()
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.text_ui_sm(cx)
|
||||
.children(rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.input.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
@@ -2720,81 +2730,83 @@ impl ActiveThread {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
})),
|
||||
),
|
||||
)
|
||||
.map(|container| match tool_use.status {
|
||||
ToolUseStatus::Finished(_) => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Result")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(div().w_full().text_ui_sm(cx).children(
|
||||
rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.map(|container| match tool_use.status {
|
||||
ToolUseStatus::Finished(_) => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Result")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(div().w_full().text_ui_sm().children(
|
||||
rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
)),
|
||||
),
|
||||
ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(_) => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(div().text_ui_sm().children(rendered_tool_use.as_ref().map(
|
||||
|rendered| {
|
||||
),
|
||||
ToolUseStatus::InputStillStreaming | ToolUseStatus::Running => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Error(_) => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Error")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui_sm(cx)
|
||||
.children(rendered_tool_use.as_ref().map(|rendered| {
|
||||
MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
@@ -2806,22 +2818,22 @@ impl ActiveThread {
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
},
|
||||
))),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
ToolUseStatus::NeedsConfirmation => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Asking Permission")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
});
|
||||
})),
|
||||
),
|
||||
),
|
||||
ToolUseStatus::Pending => container,
|
||||
ToolUseStatus::NeedsConfirmation => container.child(
|
||||
results_content_container()
|
||||
.border_t_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Asking Permission")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
let gradient_overlay = |color: Hsla| {
|
||||
div()
|
||||
@@ -2959,7 +2971,7 @@ impl ActiveThread {
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex().pr_8().text_ui_sm().children(
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}}))
|
||||
@@ -3246,18 +3258,15 @@ impl ActiveThread {
|
||||
.map(|tool_use| tool_use.status.clone())
|
||||
{
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
if let Some(configured) = thread.get_or_init_configured_model(cx) {
|
||||
thread.run_tool(
|
||||
c.tool_use_id.clone(),
|
||||
c.ui_text.clone(),
|
||||
c.input.clone(),
|
||||
&c.messages,
|
||||
c.tool.clone(),
|
||||
configured.model,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
thread.run_tool(
|
||||
c.tool_use_id.clone(),
|
||||
c.ui_text.clone(),
|
||||
c.input.clone(),
|
||||
&c.messages,
|
||||
c.tool.clone(),
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3321,12 +3330,43 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, window: &mut Window) -> Option<Stateful<Div>> {
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !self.show_scrollbar && !self.scrollbar_state.is_dragging() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(scrollbar(self.scrollbar_state.clone(), window))
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("active-thread-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn hide_scrollbar_later(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -3354,7 +3394,7 @@ pub enum ActiveThreadEvent {
|
||||
impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
@@ -3375,7 +3415,7 @@ impl Render for ActiveThread {
|
||||
}),
|
||||
)
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
.when_some(self.render_vertical_scrollbar(window), |this, scrollbar| {
|
||||
.when_some(self.render_vertical_scrollbar(cx), |this, scrollbar| {
|
||||
this.child(scrollbar)
|
||||
})
|
||||
}
|
||||
@@ -3487,7 +3527,7 @@ pub(crate) fn open_context(
|
||||
}
|
||||
|
||||
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_thread(thread_context.thread.clone(), window, cx);
|
||||
});
|
||||
@@ -3496,7 +3536,7 @@ pub(crate) fn open_context(
|
||||
|
||||
AgentContextHandle::TextThread(text_thread_context) => {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
|
||||
});
|
||||
|
||||
135
crates/agent/src/assistant.rs
Normal file
135
crates/agent/src/assistant.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
mod active_thread;
|
||||
mod agent_diff;
|
||||
mod assistant_configuration;
|
||||
mod assistant_model_selector;
|
||||
mod assistant_panel;
|
||||
mod buffer_codegen;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_server_tool;
|
||||
mod context_store;
|
||||
mod context_strip;
|
||||
mod debug;
|
||||
mod history_store;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod message_editor;
|
||||
mod profile_selector;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_compatibility;
|
||||
mod tool_use;
|
||||
mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings};
|
||||
use client::Client;
|
||||
use fs::Fs;
|
||||
use gpui::{App, actions, impl_actions};
|
||||
use language::LanguageRegistry;
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use thread::ThreadId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::context::{ContextLoadResult, LoadedContext};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use context_store::ContextStore;
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
[
|
||||
NewTextThread,
|
||||
ToggleContextPicker,
|
||||
ToggleNavigationMenu,
|
||||
ToggleOptionsMenu,
|
||||
DeleteRecentlyOpenThread,
|
||||
ToggleProfileSelector,
|
||||
RemoveAllContext,
|
||||
ExpandMessageEditor,
|
||||
OpenHistory,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
FocusUp,
|
||||
FocusDown,
|
||||
FocusLeft,
|
||||
FocusRight,
|
||||
RemoveFocusedContext,
|
||||
AcceptSuggestedContext,
|
||||
OpenActiveThreadAsMarkdown,
|
||||
OpenAgentDiff,
|
||||
Keep,
|
||||
Reject,
|
||||
RejectAll,
|
||||
KeepAll,
|
||||
Follow,
|
||||
ResetTrialUpsell,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
impl ManageProfiles {
|
||||
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
|
||||
Self {
|
||||
customize_tools: Some(profile_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_actions!(agent, [NewThread, ManageProfiles]);
|
||||
|
||||
/// Initializes the `agent` crate.
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
AssistantSettings::register(cx);
|
||||
thread_store::init(cx);
|
||||
assistant_panel::init(cx);
|
||||
context_server_configuration::init(language_registry, cx);
|
||||
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
terminal_inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
client.telemetry().clone(),
|
||||
cx,
|
||||
);
|
||||
cx.observe_new(AddContextServerModal::register).detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
}
|
||||
@@ -18,7 +18,8 @@ use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageMod
|
||||
use project::context_server_store::{ContextServerStatus, ContextServerStore};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{
|
||||
prelude::*, scrollbar, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip
|
||||
Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Scrollbar, ScrollbarState,
|
||||
Switch, SwitchColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
@@ -29,7 +30,7 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
pub struct AssistantConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
@@ -41,7 +42,7 @@ pub struct AgentConfiguration {
|
||||
scrollbar_state: ScrollbarState,
|
||||
}
|
||||
|
||||
impl AgentConfiguration {
|
||||
impl AssistantConfiguration {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
@@ -109,7 +110,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentConfiguration {
|
||||
impl Focusable for AssistantConfiguration {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
@@ -119,9 +120,9 @@ pub enum AssistantConfigurationEvent {
|
||||
NewThread(Arc<dyn LanguageModelProvider>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
|
||||
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
|
||||
|
||||
impl AgentConfiguration {
|
||||
impl AssistantConfiguration {
|
||||
fn render_provider_configuration_block(
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
@@ -570,7 +571,7 @@ impl AgentConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentConfiguration {
|
||||
impl Render for AssistantConfiguration {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("assistant-configuration")
|
||||
@@ -592,6 +593,31 @@ impl Render for AgentConfiguration {
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.child(scrollbar(self.scrollbar_state.clone(), window))
|
||||
.child(
|
||||
div()
|
||||
.id("assistant-configuration-scrollbar")
|
||||
.occlude()
|
||||
.absolute()
|
||||
.right(px(3.))
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.pb_6()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -515,7 +515,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
|
||||
font_family: Some(theme_settings.ui_font.family.clone()),
|
||||
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
|
||||
font_features: Some(theme_settings.ui_font.features.clone()),
|
||||
font_size: Some(TextSize::XSmall.rems().into()),
|
||||
font_size: Some(TextSize::XSmall.rems(cx).into()),
|
||||
color: Some(colors.text_muted),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -18,9 +18,9 @@ use ui::{
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AgentPanel, ManageProfiles, ThreadStore};
|
||||
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
|
||||
|
||||
use super::tool_picker::ToolPickerMode;
|
||||
|
||||
@@ -115,7 +115,7 @@ impl ManageProfilesModal {
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let thread_store = panel.read(cx).thread_store();
|
||||
let tools = thread_store.read(cx).tools();
|
||||
@@ -17,13 +17,13 @@ pub enum ModelType {
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl AgentModelSelector {
|
||||
impl AssistantModelSelector {
|
||||
pub(crate) fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
@@ -99,7 +99,7 @@ impl AgentModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
|
||||
AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
|
||||
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
|
||||
render_remaining_tokens,
|
||||
};
|
||||
@@ -53,8 +53,8 @@ use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFon
|
||||
use zed_llm_client::UsageLimit;
|
||||
|
||||
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
|
||||
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
|
||||
use crate::agent_diff::AgentDiff;
|
||||
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
@@ -71,7 +71,7 @@ use crate::{
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SerializedAgentPanel {
|
||||
struct SerializedAssistantPanel {
|
||||
width: Option<Pixels>,
|
||||
}
|
||||
|
||||
@@ -80,40 +80,40 @@ pub fn init(cx: &mut App) {
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace
|
||||
.register_action(|workspace, action: &NewThread, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenHistory, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.open_history(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &NewTextThread, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_rules_library(action, window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
let thread = panel.read(cx).thread.read(cx).thread().clone();
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
||||
}
|
||||
@@ -122,8 +122,8 @@ pub fn init(cx: &mut App) {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.message_editor.update(cx, |editor, cx| {
|
||||
editor.expand_message_editor(&ExpandMessageEditor, window, cx);
|
||||
@@ -132,16 +132,16 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
|
||||
});
|
||||
@@ -335,7 +335,7 @@ impl ActiveView {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AgentPanel {
|
||||
pub struct AssistantPanel {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
user_store: Entity<UserStore>,
|
||||
project: Entity<Project>,
|
||||
@@ -349,7 +349,7 @@ pub struct AgentPanel {
|
||||
context_store: Entity<TextThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
inline_assist_context_store: Entity<crate::context_store::ContextStore>,
|
||||
configuration: Option<Entity<AgentConfiguration>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
active_view: ActiveView,
|
||||
@@ -366,14 +366,14 @@ pub struct AgentPanel {
|
||||
_trial_markdown: Entity<Markdown>,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
impl AssistantPanel {
|
||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||
let width = self.width;
|
||||
self.pending_serialization = Some(cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
AGENT_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedAgentPanel { width })?,
|
||||
serde_json::to_string(&SerializedAssistantPanel { width })?,
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
@@ -423,7 +423,7 @@ impl AgentPanel {
|
||||
.log_err()
|
||||
.flatten()
|
||||
{
|
||||
Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
|
||||
Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -491,6 +491,7 @@ impl AgentPanel {
|
||||
thread_store.downgrade(),
|
||||
context_store.downgrade(),
|
||||
thread.clone(),
|
||||
agent_panel_dock_position(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -821,6 +822,7 @@ impl AgentPanel {
|
||||
self.thread_store.downgrade(),
|
||||
self.context_store.downgrade(),
|
||||
thread,
|
||||
agent_panel_dock_position(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1029,6 +1031,7 @@ impl AgentPanel {
|
||||
self.thread_store.downgrade(),
|
||||
self.context_store.downgrade(),
|
||||
thread,
|
||||
agent_panel_dock_position(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1163,13 +1166,15 @@ impl AgentPanel {
|
||||
|
||||
self.set_active_view(ActiveView::Configuration, window, cx);
|
||||
self.configuration =
|
||||
Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
|
||||
Some(cx.new(|cx| {
|
||||
AssistantConfiguration::new(fs, context_server_store, tools, window, cx)
|
||||
}));
|
||||
|
||||
if let Some(configuration) = self.configuration.as_ref() {
|
||||
self.configuration_subscription = Some(cx.subscribe_in(
|
||||
configuration,
|
||||
window,
|
||||
Self::handle_agent_configuration_event,
|
||||
Self::handle_assistant_configuration_event,
|
||||
));
|
||||
|
||||
configuration.focus_handle(cx).focus(window);
|
||||
@@ -1199,9 +1204,9 @@ impl AgentPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn handle_agent_configuration_event(
|
||||
fn handle_assistant_configuration_event(
|
||||
&mut self,
|
||||
_entity: &Entity<AgentConfiguration>,
|
||||
_entity: &Entity<AssistantConfiguration>,
|
||||
event: &AssistantConfigurationEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -1314,7 +1319,7 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentPanel {
|
||||
impl Focusable for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
||||
@@ -1339,9 +1344,9 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AgentPanel {}
|
||||
impl EventEmitter<PanelEvent> for AssistantPanel {}
|
||||
|
||||
impl Panel for AgentPanel {
|
||||
impl Panel for AssistantPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"AgentPanel"
|
||||
}
|
||||
@@ -1355,6 +1360,10 @@ impl Panel for AgentPanel {
|
||||
}
|
||||
|
||||
fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.message_editor.update(cx, |message_editor, cx| {
|
||||
message_editor.set_dock_position(position, cx);
|
||||
});
|
||||
|
||||
settings::update_settings_file::<AssistantSettings>(
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
@@ -1416,7 +1425,7 @@ impl Panel for AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
impl AssistantPanel {
|
||||
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
|
||||
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
|
||||
|
||||
@@ -1975,9 +1984,9 @@ impl AgentPanel {
|
||||
.style(ButtonStyle::Transparent)
|
||||
.color(Color::Muted)
|
||||
.on_click({
|
||||
let agent_panel = cx.entity();
|
||||
let assistant_panel = cx.entity();
|
||||
move |_, _, cx| {
|
||||
agent_panel.update(
|
||||
assistant_panel.update(
|
||||
cx,
|
||||
|this, cx| {
|
||||
let hidden =
|
||||
@@ -2336,13 +2345,14 @@ impl AgentPanel {
|
||||
""
|
||||
};
|
||||
|
||||
let banner = Banner::new()
|
||||
.severity(ui::Severity::Info)
|
||||
.child(h_flex().child(Label::new(format!(
|
||||
"Consecutive tool use limit reached.{max_mode_upsell}"
|
||||
))));
|
||||
|
||||
Some(div().px_2().pb_2().child(banner).into_any_element())
|
||||
Some(
|
||||
Banner::new()
|
||||
.severity(ui::Severity::Info)
|
||||
.child(h_flex().child(Label::new(format!(
|
||||
"Consecutive tool use limit reached.{max_mode_upsell}"
|
||||
))))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
@@ -2742,7 +2752,7 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentPanel {
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let content = match &self.active_view {
|
||||
ActiveView::Thread { .. } => v_flex()
|
||||
@@ -2853,26 +2863,28 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
|
||||
})
|
||||
}
|
||||
|
||||
fn focus_agent_panel(
|
||||
fn focus_assistant_panel(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> bool {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx).is_some()
|
||||
workspace
|
||||
.focus_panel::<AssistantPanel>(window, cx)
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConcreteAssistantPanelDelegate;
|
||||
|
||||
impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn active_context_editor(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Entity<ContextEditor>> {
|
||||
let panel = workspace.panel::<AgentPanel>(cx)?;
|
||||
let panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
panel.read(cx).active_context_editor()
|
||||
}
|
||||
|
||||
@@ -2883,7 +2895,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
|
||||
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return Task::ready(Err(anyhow!("Agent panel not found")));
|
||||
};
|
||||
|
||||
@@ -2910,12 +2922,12 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
|
||||
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !panel.focus_handle(cx).contains_focused(window, cx) {
|
||||
workspace.toggle_panel_focus::<AgentPanel>(window, cx);
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
@@ -36,7 +36,7 @@ use ui::{
|
||||
use uuid::Uuid;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AgentPanel;
|
||||
use crate::AssistantPanel;
|
||||
use crate::context::RULES_ICON;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::ThreadId;
|
||||
@@ -648,7 +648,7 @@ fn recent_context_picker_entries(
|
||||
let current_threads = context_store.read(cx).thread_ids();
|
||||
|
||||
let active_thread_id = workspace
|
||||
.panel::<AgentPanel>(cx)
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
|
||||
|
||||
if let Some((thread_store, text_thread_store)) = thread_store
|
||||
|
||||
@@ -10,7 +10,7 @@ use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::agent_configuration::ConfigureContextServerModal;
|
||||
use crate::assistant_configuration::ConfigureContextServerModal;
|
||||
|
||||
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
|
||||
cx.observe_new(move |_: &mut Workspace, window, cx| {
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, bail};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
|
||||
use context_server::{ContextServerId, types};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, context_server_store::ContextServerStore};
|
||||
use ui::IconName;
|
||||
|
||||
@@ -75,7 +75,6 @@ impl Tool for ContextServerTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::thread::Thread;
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use crate::ui::{AddedContext, ContextPill};
|
||||
use crate::{
|
||||
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
||||
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
||||
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
||||
};
|
||||
|
||||
@@ -144,7 +144,7 @@ impl ContextStrip {
|
||||
}
|
||||
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
|
||||
let panel = workspace.read(cx).panel::<AssistantPanel>(cx)?.read(cx);
|
||||
|
||||
if let Some(active_thread) = panel.active_thread() {
|
||||
let weak_active_thread = active_thread.downgrade();
|
||||
|
||||
@@ -8,10 +8,9 @@ use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::display_map::EditorMargins;
|
||||
use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
actions::SelectAll,
|
||||
display_map::{
|
||||
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
@@ -43,7 +42,7 @@ use util::ResultExt;
|
||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
|
||||
use crate::AgentPanel;
|
||||
use crate::AssistantPanel;
|
||||
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
|
||||
@@ -182,12 +181,13 @@ impl InlineAssistant {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if is_assistant2_enabled {
|
||||
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
|
||||
let panel = workspace.read(cx).panel::<AssistantPanel>(cx);
|
||||
let thread_store = panel
|
||||
.as_ref()
|
||||
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
|
||||
let text_thread_store = panel
|
||||
.map(|agent_panel| agent_panel.read(cx).text_thread_store().downgrade());
|
||||
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
|
||||
let text_thread_store = panel.map(|assistant_panel| {
|
||||
assistant_panel.read(cx).text_thread_store().downgrade()
|
||||
});
|
||||
|
||||
editor.add_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
@@ -226,7 +226,7 @@ impl InlineAssistant {
|
||||
|
||||
let Some(inline_assist_target) = Self::resolve_inline_assist_target(
|
||||
workspace,
|
||||
workspace.panel::<AgentPanel>(cx),
|
||||
workspace.panel::<AssistantPanel>(cx),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
@@ -239,15 +239,15 @@ impl InlineAssistant {
|
||||
.map_or(false, |model| model.provider.is_authenticated(cx))
|
||||
};
|
||||
|
||||
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
|
||||
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let agent_panel = agent_panel.read(cx);
|
||||
let assistant_panel = assistant_panel.read(cx);
|
||||
|
||||
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
||||
let thread_store = Some(agent_panel.thread_store().downgrade());
|
||||
let text_thread_store = Some(agent_panel.text_thread_store().downgrade());
|
||||
let context_store = agent_panel.inline_assist_context_store().clone();
|
||||
let prompt_store = assistant_panel.prompt_store().as_ref().cloned();
|
||||
let thread_store = Some(assistant_panel.thread_store().downgrade());
|
||||
let text_thread_store = Some(assistant_panel.text_thread_store().downgrade());
|
||||
let context_store = assistant_panel.inline_assist_context_store().clone();
|
||||
|
||||
let handle_assist =
|
||||
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
|
||||
@@ -458,11 +458,11 @@ impl InlineAssistant {
|
||||
)
|
||||
});
|
||||
|
||||
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
|
||||
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
PromptEditor::new_buffer(
|
||||
assist_id,
|
||||
editor_margins,
|
||||
gutter_dimensions.clone(),
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen.clone(),
|
||||
@@ -577,11 +577,11 @@ impl InlineAssistant {
|
||||
)
|
||||
});
|
||||
|
||||
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
|
||||
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
PromptEditor::new_buffer(
|
||||
assist_id,
|
||||
editor_margins,
|
||||
gutter_dimensions.clone(),
|
||||
self.prompt_history.clone(),
|
||||
prompt_buffer.clone(),
|
||||
codegen.clone(),
|
||||
@@ -650,7 +650,6 @@ impl InlineAssistant {
|
||||
height: Some(prompt_editor_height),
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
@@ -665,7 +664,6 @@ impl InlineAssistant {
|
||||
.into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1407,11 +1405,11 @@ impl InlineAssistant {
|
||||
|
||||
enum DeletedLines {}
|
||||
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
|
||||
editor.disable_scrollbars_and_minimap(cx);
|
||||
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor.highlight_rows::<DeletedLines>(
|
||||
@@ -1435,12 +1433,11 @@ impl InlineAssistant {
|
||||
.bg(cx.theme().status().deleted_background)
|
||||
.size_full()
|
||||
.h(height as f32 * cx.window.line_height())
|
||||
.pl(cx.margins.gutter.full_width())
|
||||
.pl(cx.gutter_dimensions.full_width())
|
||||
.child(deleted_lines_editor.clone())
|
||||
.into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1453,7 +1450,7 @@ impl InlineAssistant {
|
||||
|
||||
fn resolve_inline_assist_target(
|
||||
workspace: &mut Workspace,
|
||||
agent_panel: Option<Entity<AgentPanel>>,
|
||||
assistant_panel: Option<Entity<AssistantPanel>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<InlineAssistTarget> {
|
||||
@@ -1473,7 +1470,7 @@ impl InlineAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
let context_editor = agent_panel
|
||||
let context_editor = assistant_panel
|
||||
.and_then(|panel| panel.read(cx).active_context_editor())
|
||||
.and_then(|editor| {
|
||||
let editor = &editor.read(cx).editor().clone();
|
||||
@@ -1598,9 +1595,9 @@ fn build_assist_editor_renderer(editor: &Entity<PromptEditor<BufferCodegen>>) ->
|
||||
let editor = editor.clone();
|
||||
|
||||
Arc::new(move |cx: &mut BlockContext| {
|
||||
let editor_margins = editor.read(cx).editor_margins();
|
||||
let gutter_dimensions = editor.read(cx).gutter_dimensions();
|
||||
|
||||
*editor_margins.lock() = *cx.margins;
|
||||
*gutter_dimensions.lock() = *cx.gutter_dimensions;
|
||||
editor.clone().into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context::ContextCreasesAddon;
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
|
||||
@@ -11,9 +11,9 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use editor::display_map::EditorMargins;
|
||||
use editor::{
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
|
||||
GutterDimensions, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
|
||||
@@ -42,7 +42,7 @@ pub struct PromptEditor<T> {
|
||||
context_store: Entity<ContextStore>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AgentModelSelector>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
edited_since_done: bool,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_history_ix: Option<usize>,
|
||||
@@ -61,13 +61,11 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||
let mut buttons = Vec::new();
|
||||
|
||||
const RIGHT_PADDING: Pixels = px(9.);
|
||||
|
||||
let (left_gutter_width, right_padding) = match &self.mode {
|
||||
let left_gutter_width = match &self.mode {
|
||||
PromptEditorMode::Buffer {
|
||||
id: _,
|
||||
codegen,
|
||||
editor_margins,
|
||||
gutter_dimensions,
|
||||
} => {
|
||||
let codegen = codegen.read(cx);
|
||||
|
||||
@@ -75,17 +73,13 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
buttons.push(self.render_cycle_controls(&codegen, cx));
|
||||
}
|
||||
|
||||
let editor_margins = editor_margins.lock();
|
||||
let gutter = editor_margins.gutter;
|
||||
let gutter_dimensions = gutter_dimensions.lock();
|
||||
|
||||
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
|
||||
let right_padding = editor_margins.right + RIGHT_PADDING;
|
||||
|
||||
(left_gutter_width, right_padding)
|
||||
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
|
||||
}
|
||||
PromptEditorMode::Terminal { .. } => {
|
||||
// Give the equivalent of the same left-padding that we're using on the right
|
||||
(Pixels::from(40.0), Pixels::from(24.))
|
||||
Pixels::from(40.0)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,7 +100,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.size_full()
|
||||
.pt_0p5()
|
||||
.pb(bottom_padding)
|
||||
.pr(right_padding)
|
||||
.pr_6()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
@@ -290,12 +284,12 @@ impl<T: 'static> PromptEditor<T> {
|
||||
PromptEditorMode::Terminal { .. } => "Generate",
|
||||
};
|
||||
|
||||
let agent_panel_keybinding =
|
||||
let assistant_panel_keybinding =
|
||||
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
|
||||
.map(|keybinding| format!("{keybinding} to chat ― "))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
|
||||
format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
|
||||
}
|
||||
|
||||
pub fn prompt(&self, cx: &App) -> String {
|
||||
@@ -759,7 +753,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
}
|
||||
|
||||
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let font_size = TextSize::Default.rems();
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
|
||||
|
||||
div()
|
||||
@@ -812,7 +806,7 @@ pub enum PromptEditorMode {
|
||||
Buffer {
|
||||
id: InlineAssistId,
|
||||
codegen: Entity<BufferCodegen>,
|
||||
editor_margins: Arc<Mutex<EditorMargins>>,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
},
|
||||
Terminal {
|
||||
id: TerminalInlineAssistId,
|
||||
@@ -844,7 +838,7 @@ impl InlineAssistId {
|
||||
impl PromptEditor<BufferCodegen> {
|
||||
pub fn new_buffer(
|
||||
id: InlineAssistId,
|
||||
editor_margins: Arc<Mutex<EditorMargins>>,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
prompt_history: VecDeque<String>,
|
||||
prompt_buffer: Entity<MultiBuffer>,
|
||||
codegen: Entity<BufferCodegen>,
|
||||
@@ -861,7 +855,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
let mode = PromptEditorMode::Buffer {
|
||||
id,
|
||||
codegen,
|
||||
editor_margins,
|
||||
gutter_dimensions,
|
||||
};
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
@@ -927,7 +921,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AgentModelSelector::new(
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle,
|
||||
prompt_editor.focus_handle(cx),
|
||||
@@ -1001,9 +995,11 @@ impl PromptEditor<BufferCodegen> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
|
||||
pub fn gutter_dimensions(&self) -> &Arc<Mutex<GutterDimensions>> {
|
||||
match &self.mode {
|
||||
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
|
||||
PromptEditorMode::Buffer {
|
||||
gutter_dimensions, ..
|
||||
} => gutter_dimensions,
|
||||
PromptEditorMode::Terminal { .. } => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1098,7 +1094,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AgentModelSelector::new(
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
model_selector_menu_handle.clone(),
|
||||
prompt_editor.focus_handle(cx),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agent_model_selector::{AgentModelSelector, ModelType};
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use crate::ui::{
|
||||
@@ -38,8 +38,9 @@ use proto::Plan;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use ui::{Disclosure, DocumentationSide, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::dock::DockPosition;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
@@ -65,7 +66,7 @@ pub struct MessageEditor {
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AgentModelSelector>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
last_loaded_context: Option<ContextLoadResult>,
|
||||
load_context_task: Option<Shared<Task<()>>>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
@@ -132,6 +133,14 @@ pub(crate) fn create_editor(
|
||||
editor
|
||||
}
|
||||
|
||||
fn documentation_side(position: DockPosition) -> DocumentationSide {
|
||||
match position {
|
||||
DockPosition::Left => DocumentationSide::Right,
|
||||
DockPosition::Bottom => DocumentationSide::Left,
|
||||
DockPosition::Right => DocumentationSide::Left,
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -142,6 +151,7 @@ impl MessageEditor {
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
text_thread_store: WeakEntity<TextThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
dock_position: DockPosition,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -189,7 +199,7 @@ impl MessageEditor {
|
||||
];
|
||||
|
||||
let model_selector = cx.new(|cx| {
|
||||
AgentModelSelector::new(
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
@@ -199,10 +209,6 @@ impl MessageEditor {
|
||||
)
|
||||
});
|
||||
|
||||
let profile_selector = cx.new(|cx| {
|
||||
ProfileSelector::new(thread.clone(), thread_store, editor.focus_handle(cx), cx)
|
||||
});
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
@@ -219,7 +225,15 @@ impl MessageEditor {
|
||||
model_selector,
|
||||
edits_expanded: false,
|
||||
editor_is_expanded: false,
|
||||
profile_selector,
|
||||
profile_selector: cx.new(|cx| {
|
||||
ProfileSelector::new(
|
||||
fs,
|
||||
thread_store,
|
||||
editor.focus_handle(cx),
|
||||
documentation_side(dock_position),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
last_estimated_token_count: None,
|
||||
update_token_count_task: None,
|
||||
_subscriptions: subscriptions,
|
||||
@@ -629,7 +643,7 @@ impl MessageEditor {
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = TextSize::Small
|
||||
.rems()
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx));
|
||||
let line_height = settings.buffer_line_height.value() * font_size;
|
||||
|
||||
@@ -1269,6 +1283,12 @@ impl MessageEditor {
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn set_dock_position(&mut self, position: DockPosition, cx: &mut Context<Self>) {
|
||||
self.profile_selector.update(cx, |profile_selector, cx| {
|
||||
profile_selector.set_documentation_side(documentation_side(position), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extract_message_creases(
|
||||
@@ -1343,7 +1363,7 @@ impl Render for MessageEditor {
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
let line_height = TextSize::Small.rems().to_pixels(window.rem_size()) * 1.5;
|
||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
@@ -1442,6 +1462,7 @@ impl AgentPreview for MessageEditor {
|
||||
thread_store.downgrade(),
|
||||
text_thread_store.downgrade(),
|
||||
thread,
|
||||
DockPosition::Left,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileId, AssistantDockPosition, AssistantSettings, GroupedAgentProfiles,
|
||||
builtin_profiles,
|
||||
AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
|
||||
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: GroupedAgentProfiles,
|
||||
thread: Entity<Thread>,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
documentation_side: DocumentationSide,
|
||||
}
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
thread: Entity<Thread>,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
documentation_side: DocumentationSide,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
@@ -35,14 +39,20 @@ impl ProfileSelector {
|
||||
|
||||
Self {
|
||||
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
|
||||
thread,
|
||||
fs,
|
||||
thread_store,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
focus_handle,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
documentation_side,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_documentation_side(&mut self, side: DocumentationSide, cx: &mut Context<Self>) {
|
||||
self.documentation_side = side;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
|
||||
self.menu_handle.clone()
|
||||
}
|
||||
@@ -102,7 +112,7 @@ impl ProfileSelector {
|
||||
.toggleable(IconPosition::End, profile_id == settings.default_profile);
|
||||
|
||||
let entry = if let Some(doc_text) = documentation {
|
||||
entry.documentation_aside(documentation_side(settings.dock), move |_| {
|
||||
entry.documentation_aside(self.documentation_side, move |_| {
|
||||
Label::new(doc_text).into_any_element()
|
||||
})
|
||||
} else {
|
||||
@@ -110,15 +120,15 @@ impl ProfileSelector {
|
||||
};
|
||||
|
||||
entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
let profile = profile.clone();
|
||||
|
||||
let thread = self.thread.clone();
|
||||
|
||||
move |_window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_configured_profile(Some(profile.clone()), cx);
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
@@ -134,80 +144,58 @@ impl ProfileSelector {
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.configured_profile())
|
||||
.or_else(|| {
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
profile.cloned()
|
||||
});
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
|
||||
let selected_profile = profile
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let configured_model = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.configured_model())
|
||||
.or_else(|| {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
model_registry.default_model()
|
||||
});
|
||||
let supports_tools =
|
||||
configured_model.map_or(false, |default| default.model.supports_tools());
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let supports_tools = model_registry
|
||||
.default_model()
|
||||
.map_or(false, |default| default.model.supports_tools());
|
||||
|
||||
if supports_tools {
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let trigger_button = Button::new("profile-selector-model", selected_profile)
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let trigger_button = if supports_tools {
|
||||
Button::new("profile-selector-model", selected_profile)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted);
|
||||
|
||||
PopoverMenu::new("profile-selector")
|
||||
.trigger_with_tooltip(trigger_button, {
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Profile Menu",
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.anchor(
|
||||
if documentation_side(settings.dock) == DocumentationSide::Left {
|
||||
gpui::Corner::BottomRight
|
||||
} else {
|
||||
gpui::Corner::BottomLeft
|
||||
},
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.into_any_element()
|
||||
.icon_color(Color::Muted)
|
||||
} else {
|
||||
Button::new("tools-not-supported-button", "Tools Unsupported")
|
||||
Button::new("tools-not-supported-button", "No Tools")
|
||||
.disabled(true)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.tooltip(Tooltip::text("This model does not support tools."))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
}
|
||||
.tooltip(Tooltip::text("The current model does not support tools."))
|
||||
};
|
||||
|
||||
fn documentation_side(position: AssistantDockPosition) -> DocumentationSide {
|
||||
match position {
|
||||
AssistantDockPosition::Left => DocumentationSide::Right,
|
||||
AssistantDockPosition::Bottom => DocumentationSide::Left,
|
||||
AssistantDockPosition::Right => DocumentationSide::Left,
|
||||
PopoverMenu::new("profile-selector")
|
||||
.trigger_with_tooltip(trigger_button, {
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Toggle Profile Menu",
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.anchor(if self.documentation_side == DocumentationSide::Left {
|
||||
gpui::Corner::BottomRight
|
||||
} else {
|
||||
gpui::Corner::BottomLeft
|
||||
})
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -359,7 +359,6 @@ pub struct Thread {
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
configured_profile: Option<AgentProfile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -380,9 +379,6 @@ impl Thread {
|
||||
) -> Self {
|
||||
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
|
||||
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
let profile_id = &assistant_settings.default_profile;
|
||||
let configured_profile = assistant_settings.profiles.get(profile_id).cloned();
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
@@ -425,7 +421,6 @@ impl Thread {
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_profile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,13 +468,6 @@ impl Thread {
|
||||
.completion_mode
|
||||
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
|
||||
|
||||
let configured_profile = serialized.profile.and_then(|profile| {
|
||||
AssistantSettings::get_global(cx)
|
||||
.profiles
|
||||
.get(&profile)
|
||||
.cloned()
|
||||
});
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: serialized.updated_at,
|
||||
@@ -553,7 +541,6 @@ impl Thread {
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_profile,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,19 +596,6 @@ impl Thread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn configured_profile(&self) -> Option<AgentProfile> {
|
||||
self.configured_profile.clone()
|
||||
}
|
||||
|
||||
pub fn set_configured_profile(
|
||||
&mut self,
|
||||
profile: Option<AgentProfile>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.configured_profile = profile;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
@@ -1126,10 +1100,6 @@ impl Thread {
|
||||
provider: model.provider.id().0.to_string(),
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
profile: this
|
||||
.configured_profile
|
||||
.as_ref()
|
||||
.map(|profile| AgentProfileId(profile.name.clone().into())),
|
||||
completion_mode: Some(this.completion_mode),
|
||||
})
|
||||
})
|
||||
@@ -1576,9 +1546,9 @@ impl Thread {
|
||||
completion.queue_state = QueueState::Started;
|
||||
}
|
||||
CompletionRequestStatus::Failed {
|
||||
code, message, request_id
|
||||
code, message
|
||||
} => {
|
||||
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
|
||||
return Err(anyhow!("completion request failed. code: {code}, message: {message}"));
|
||||
}
|
||||
CompletionRequestStatus::UsageUpdated {
|
||||
amount, limit
|
||||
@@ -1918,7 +1888,7 @@ impl Thread {
|
||||
model: Arc<dyn LanguageModel>,
|
||||
) -> Vec<PendingToolUse> {
|
||||
self.auto_capture_telemetry(cx);
|
||||
let request = self.to_completion_request(model.clone(), cx);
|
||||
let request = self.to_completion_request(model, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
let pending_tool_uses = self
|
||||
.tool_use
|
||||
@@ -1948,7 +1918,6 @@ impl Thread {
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
tool,
|
||||
model.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -2043,19 +2012,10 @@ impl Thread {
|
||||
input: serde_json::Value,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
tool: Arc<dyn Tool>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Thread>,
|
||||
) {
|
||||
let task = self.spawn_tool_use(
|
||||
tool_use_id.clone(),
|
||||
messages,
|
||||
input,
|
||||
tool,
|
||||
model,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
|
||||
self.tool_use
|
||||
.run_pending_tool(tool_use_id, ui_text.into(), task);
|
||||
}
|
||||
@@ -2066,7 +2026,6 @@ impl Thread {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
input: serde_json::Value,
|
||||
tool: Arc<dyn Tool>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut Context<Thread>,
|
||||
) -> Task<()> {
|
||||
@@ -2080,7 +2039,6 @@ impl Thread {
|
||||
messages,
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
model,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -13,16 +13,16 @@ use gpui::{
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||
Tooltip, prelude::*, scrollbar,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
use crate::{AgentPanel, RemoveSelectedThread};
|
||||
use crate::{AssistantPanel, RemoveSelectedThread};
|
||||
|
||||
pub struct ThreadHistory {
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
@@ -69,7 +69,7 @@ impl HistoryListItem {
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -96,7 +96,7 @@ impl ThreadHistory {
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
agent_panel,
|
||||
assistant_panel,
|
||||
history_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
@@ -340,33 +340,54 @@ impl ThreadHistory {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_scrollbar(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Stateful<Div>> {
|
||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
scrollbar(self.scrollbar_state.clone(), window)
|
||||
div()
|
||||
.occlude()
|
||||
.id("thread-history-scroll")
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.8))
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.pl_1(),
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w_4()
|
||||
.pl_1()
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self.agent_panel.update(cx, move |this, cx| {
|
||||
HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_thread_by_id(&thread.id, window, cx)
|
||||
}),
|
||||
HistoryEntry::Context(context) => self.agent_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
}),
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(task) = task_result.log_err() {
|
||||
@@ -386,10 +407,10 @@ impl ThreadHistory {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.agent_panel
|
||||
.assistant_panel
|
||||
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
|
||||
HistoryEntry::Context(context) => self
|
||||
.agent_panel
|
||||
.assistant_panel
|
||||
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
|
||||
};
|
||||
|
||||
@@ -485,7 +506,7 @@ impl ThreadHistory {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
self.agent_panel.clone(),
|
||||
self.assistant_panel.clone(),
|
||||
is_active,
|
||||
highlight_positions,
|
||||
format,
|
||||
@@ -493,7 +514,7 @@ impl ThreadHistory {
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
self.agent_panel.clone(),
|
||||
self.assistant_panel.clone(),
|
||||
is_active,
|
||||
highlight_positions,
|
||||
format,
|
||||
@@ -510,7 +531,7 @@ impl Focusable for ThreadHistory {
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
@@ -573,7 +594,7 @@ impl Render for ThreadHistory {
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
.when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
|
||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
||||
div.child(scrollbar)
|
||||
})
|
||||
}
|
||||
@@ -584,7 +605,7 @@ impl Render for ThreadHistory {
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: SerializedThreadMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
@@ -593,14 +614,14 @@ pub struct PastThread {
|
||||
impl PastThread {
|
||||
pub fn new(
|
||||
thread: SerializedThreadMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
agent_panel,
|
||||
assistant_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
timestamp_format,
|
||||
@@ -613,7 +634,7 @@ impl RenderOnce for PastThread {
|
||||
let summary = self.thread.summary;
|
||||
|
||||
let thread_timestamp = self.timestamp_format.format_timestamp(
|
||||
&self.agent_panel,
|
||||
&self.assistant_panel,
|
||||
self.thread.updated_at.timestamp(),
|
||||
cx,
|
||||
);
|
||||
@@ -646,10 +667,10 @@ impl RenderOnce for PastThread {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, _window, cx| {
|
||||
agent_panel
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx).detach_and_log_err(cx);
|
||||
})
|
||||
@@ -659,10 +680,10 @@ impl RenderOnce for PastThread {
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, window, cx| {
|
||||
agent_panel
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -676,7 +697,7 @@ impl RenderOnce for PastThread {
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastContext {
|
||||
context: SavedContextMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
@@ -685,14 +706,14 @@ pub struct PastContext {
|
||||
impl PastContext {
|
||||
pub fn new(
|
||||
context: SavedContextMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
agent_panel,
|
||||
assistant_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
timestamp_format,
|
||||
@@ -704,7 +725,7 @@ impl RenderOnce for PastContext {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.context.title;
|
||||
let context_timestamp = self.timestamp_format.format_timestamp(
|
||||
&self.agent_panel,
|
||||
&self.assistant_panel,
|
||||
self.context.mtime.timestamp(),
|
||||
cx,
|
||||
);
|
||||
@@ -739,10 +760,10 @@ impl RenderOnce for PastContext {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, _window, cx| {
|
||||
agent_panel
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -753,10 +774,10 @@ impl RenderOnce for PastContext {
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, window, cx| {
|
||||
agent_panel
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -776,12 +797,12 @@ pub enum EntryTimeFormat {
|
||||
impl EntryTimeFormat {
|
||||
fn format_timestamp(
|
||||
&self,
|
||||
agent_panel: &WeakEntity<AgentPanel>,
|
||||
assistant_panel: &WeakEntity<AssistantPanel>,
|
||||
timestamp: i64,
|
||||
cx: &App,
|
||||
) -> String {
|
||||
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
|
||||
let timezone = agent_panel
|
||||
let timezone = assistant_panel
|
||||
.read_with(cx, |this, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC);
|
||||
|
||||
|
||||
@@ -657,8 +657,6 @@ pub struct SerializedThread {
|
||||
pub model: Option<SerializedLanguageModel>,
|
||||
#[serde(default)]
|
||||
pub completion_mode: Option<CompletionMode>,
|
||||
#[serde(default)]
|
||||
pub profile: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -804,7 +802,6 @@ impl LegacySerializedThread {
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
profile: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ impl Render for AgentNotification {
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.elevation_3(cx)
|
||||
.text_ui()
|
||||
.text_ui(cx)
|
||||
.font(ui_font)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_xl()
|
||||
|
||||
@@ -4,7 +4,7 @@ use gpui::{
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::AgentPanel;
|
||||
use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
macro_rules! agent_onboarding_event {
|
||||
($name:expr) => {
|
||||
@@ -31,7 +31,7 @@ impl AgentOnboardingModal {
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
@@ -92,10 +92,10 @@ impl Render for AgentOnboardingModal {
|
||||
.top_0()
|
||||
.right(px(-1.0))
|
||||
.w(px(441.))
|
||||
.h(px(240.))
|
||||
.h(px(167.))
|
||||
.child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(240.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.05))),
|
||||
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -110,23 +110,6 @@ impl Render for AgentOnboardingModal {
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::linear_gradient(
|
||||
175.,
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background,
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.8,
|
||||
),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
|
||||
@@ -44,13 +44,13 @@ impl RenderOnce for UsageCallout {
|
||||
"Upgrade to continue, wait for the next reset, or switch to API key."
|
||||
.to_string(),
|
||||
"Upgrade",
|
||||
zed_urls::account_url(cx),
|
||||
zed_urls::account_upgrade_url(cx),
|
||||
),
|
||||
Plan::ZedProTrial => (
|
||||
"Out of trial prompts",
|
||||
"Upgrade to Zed Pro to continue, or switch to API key.".to_string(),
|
||||
"Upgrade",
|
||||
zed_urls::account_url(cx),
|
||||
zed_urls::account_upgrade_url(cx),
|
||||
),
|
||||
Plan::ZedPro => (
|
||||
"Out of included prompts",
|
||||
@@ -67,7 +67,7 @@ impl RenderOnce for UsageCallout {
|
||||
"{remaining} remaining - Upgrade to increase limit, or switch providers",
|
||||
),
|
||||
"Upgrade",
|
||||
zed_urls::account_url(cx),
|
||||
zed_urls::account_upgrade_url(cx),
|
||||
),
|
||||
Plan::ZedProTrial => (
|
||||
"Reaching trial limit soon",
|
||||
@@ -75,7 +75,7 @@ impl RenderOnce for UsageCallout {
|
||||
"{remaining} remaining - Upgrade to increase limit, or switch providers",
|
||||
),
|
||||
"Upgrade",
|
||||
zed_urls::account_url(cx),
|
||||
zed_urls::account_upgrade_url(cx),
|
||||
),
|
||||
_ => return div().into_any_element(),
|
||||
}
|
||||
|
||||
88
crates/assistant/Cargo.toml
Normal file
88
crates/assistant/Cargo.toml
Normal file
@@ -0,0 +1,88 @@
|
||||
[package]
|
||||
name = "assistant"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"editor/test-support",
|
||||
"language/test-support",
|
||||
"project/test-support",
|
||||
"text/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
rules_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
languages = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
terminal_view = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-md.workspace = true
|
||||
unindent.workspace = true
|
||||
1
crates/assistant/LICENSE-GPL
Symbolic link
1
crates/assistant/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -1,123 +1,102 @@
|
||||
mod active_thread;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
mod agent_panel;
|
||||
mod buffer_codegen;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_server_tool;
|
||||
mod context_store;
|
||||
mod context_strip;
|
||||
mod debug;
|
||||
mod history_store;
|
||||
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
|
||||
|
||||
mod assistant_configuration;
|
||||
pub mod assistant_panel;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod message_editor;
|
||||
mod profile_selector;
|
||||
mod slash_command_settings;
|
||||
mod terminal_codegen;
|
||||
pub mod slash_command_settings;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_compatibility;
|
||||
mod tool_use;
|
||||
mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings, LanguageModelSelection};
|
||||
use assistant_settings::{AssistantSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{App, actions, impl_actions};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions};
|
||||
use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use thread::ThreadId;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::agent_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::context::{ContextLoadResult, LoadedContext};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||
pub(crate) use crate::inline_assistant::*;
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use context_store::ContextStore;
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
|
||||
actions!(
|
||||
agent,
|
||||
assistant,
|
||||
[
|
||||
NewTextThread,
|
||||
ToggleContextPicker,
|
||||
ToggleNavigationMenu,
|
||||
ToggleOptionsMenu,
|
||||
DeleteRecentlyOpenThread,
|
||||
ToggleProfileSelector,
|
||||
RemoveAllContext,
|
||||
ExpandMessageEditor,
|
||||
OpenHistory,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
InsertActivePrompt,
|
||||
DeployHistory,
|
||||
NewChat,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
FocusUp,
|
||||
FocusDown,
|
||||
FocusLeft,
|
||||
FocusRight,
|
||||
RemoveFocusedContext,
|
||||
AcceptSuggestedContext,
|
||||
OpenActiveThreadAsMarkdown,
|
||||
OpenAgentDiff,
|
||||
Keep,
|
||||
Reject,
|
||||
RejectAll,
|
||||
KeepAll,
|
||||
Follow,
|
||||
ResetTrialUpsell,
|
||||
CyclePreviousInlineAssist
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
const DEFAULT_CONTEXT_LINES: usize = 50;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct LanguageModelUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<AgentProfileId>,
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct LanguageModelChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: LanguageModelResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl ManageProfiles {
|
||||
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
|
||||
Self {
|
||||
customize_tools: Some(profile_id),
|
||||
/// The state pertaining to the Assistant.
|
||||
#[derive(Default)]
|
||||
struct Assistant {
|
||||
/// Whether the Assistant is enabled.
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for Assistant {}
|
||||
|
||||
impl Assistant {
|
||||
const NAMESPACE: &'static str = "assistant";
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool, cx: &mut App) {
|
||||
if self.enabled == enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.enabled = enabled;
|
||||
|
||||
if !enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Self::NAMESPACE);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_namespace(Self::NAMESPACE);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn enabled(cx: &App) -> bool {
|
||||
Self::global(cx).enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl_actions!(agent, [NewThread, ManageProfiles]);
|
||||
|
||||
/// Initializes the `agent` crate.
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
@@ -125,9 +104,8 @@ pub fn init(
|
||||
rules_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
assistant_slash_command::init(cx);
|
||||
thread_store::init(cx);
|
||||
agent_panel::init(cx);
|
||||
context_server_configuration::init(language_registry, cx);
|
||||
assistant_tool::init(cx);
|
||||
assistant_panel::init(cx);
|
||||
|
||||
register_slash_commands(cx);
|
||||
inline_assistant::init(
|
||||
@@ -143,8 +121,22 @@ pub fn init(
|
||||
cx,
|
||||
);
|
||||
indexed_docs::init(cx);
|
||||
cx.observe_new(AddContextServerModal::register).detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(Assistant::NAMESPACE);
|
||||
});
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
cx.observe_global::<SettingsStore>(|cx| {
|
||||
Assistant::update_global(cx, |assistant, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
assistant.set_enabled(settings.enabled, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn init_language_model_settings(cx: &mut App) {
|
||||
@@ -258,3 +250,11 @@ fn update_slash_commands_from_settings(cx: &mut App) {
|
||||
.unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
199
crates/assistant/src/assistant_configuration.rs
Normal file
199
crates/assistant/src/assistant_configuration.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{ElevationIndex, prelude::*};
|
||||
use workspace::Item;
|
||||
|
||||
pub struct ConfigurationView {
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
|
||||
_registry_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ConfigurationView {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let registry_subscription = cx.subscribe_in(
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
|this, _, event: &language_model::Event, window, cx| match event {
|
||||
language_model::Event::AddedProvider(provider_id) => {
|
||||
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
|
||||
if let Some(provider) = provider {
|
||||
this.add_configuration_view(&provider, window, cx);
|
||||
}
|
||||
}
|
||||
language_model::Event::RemovedProvider(provider_id) => {
|
||||
this.remove_configuration_view(provider_id);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
let mut this = Self {
|
||||
focus_handle,
|
||||
configuration_views: HashMap::default(),
|
||||
_registry_subscription: registry_subscription,
|
||||
};
|
||||
this.build_configuration_views(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn build_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
for provider in providers {
|
||||
self.add_configuration_view(&provider, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
|
||||
self.configuration_views.remove(provider_id);
|
||||
}
|
||||
|
||||
fn add_configuration_view(
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let configuration_view = provider.configuration_view(window, cx);
|
||||
self.configuration_views
|
||||
.insert(provider.id(), configuration_view);
|
||||
}
|
||||
|
||||
fn render_provider_view(
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Div {
|
||||
let provider_id = provider.id().0.clone();
|
||||
let provider_name = provider.name().0.clone();
|
||||
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
|
||||
|
||||
let open_new_context = cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_, _, _window, cx| {
|
||||
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
|
||||
.when(provider.is_authenticated(cx), move |this| {
|
||||
this.child(
|
||||
h_flex().justify_end().child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-context-{provider_id}")),
|
||||
"Open New Chat",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.on_click(open_new_context),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_sm()
|
||||
.when(configuration_view.is_none(), |this| {
|
||||
this.child(div().child(Label::new(format!(
|
||||
"No configuration view for {}",
|
||||
provider_name
|
||||
))))
|
||||
})
|
||||
.when_some(configuration_view, |this, configuration_view| {
|
||||
this.child(configuration_view)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let providers = LanguageModelRegistry::read_global(cx).providers();
|
||||
let provider_views = providers
|
||||
.into_iter()
|
||||
.map(|provider| self.render_provider_view(&provider, cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut element = v_flex()
|
||||
.id("assistant-configuration-view")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap_1()
|
||||
.child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
|
||||
.child(
|
||||
Label::new(
|
||||
"At least one LLM provider must be configured to use the Assistant.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.mt_1()
|
||||
.gap_6()
|
||||
.flex_1()
|
||||
.children(provider_views),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
// We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
|
||||
// because we couldn't the element to take up the size of the parent.
|
||||
canvas(
|
||||
move |bounds, window, cx| {
|
||||
element.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
|
||||
element
|
||||
},
|
||||
|_, mut element, window, cx| {
|
||||
element.paint(window, cx);
|
||||
},
|
||||
)
|
||||
.flex_1()
|
||||
.w_full()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ConfigurationViewEvent {
|
||||
NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
|
||||
}
|
||||
|
||||
impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
|
||||
|
||||
impl Focusable for ConfigurationView {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for ConfigurationView {
|
||||
type Event = ConfigurationViewEvent;
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Configuration".into()
|
||||
}
|
||||
}
|
||||
1450
crates/assistant/src/assistant_panel.rs
Normal file
1450
crates/assistant/src/assistant_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
4126
crates/assistant/src/inline_assistant.rs
Normal file
4126
crates/assistant/src/inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
1275
crates/assistant/src/terminal_inline_assistant.rs
Normal file
1275
crates/assistant/src/terminal_inline_assistant.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -137,7 +137,7 @@ pub enum ThoughtProcessStatus {
|
||||
Completed,
|
||||
}
|
||||
|
||||
pub trait AgentPanelDelegate {
|
||||
pub trait AssistantPanelDelegate {
|
||||
fn active_context_editor(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
@@ -171,7 +171,7 @@ pub trait AgentPanelDelegate {
|
||||
);
|
||||
}
|
||||
|
||||
impl dyn AgentPanelDelegate {
|
||||
impl dyn AssistantPanelDelegate {
|
||||
/// Returns the global [`AssistantPanelDelegate`], if it exists.
|
||||
pub fn try_global(cx: &App) -> Option<Arc<Self>> {
|
||||
cx.try_global::<GlobalAssistantPanelDelegate>()
|
||||
@@ -184,7 +184,7 @@ impl dyn AgentPanelDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
|
||||
struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
|
||||
|
||||
impl Global for GlobalAssistantPanelDelegate {}
|
||||
|
||||
@@ -242,9 +242,9 @@ impl ContextEditor {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx);
|
||||
editor.disable_scrollbars_and_minimap(cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
@@ -367,16 +367,10 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.sending_disabled(cx) {
|
||||
return;
|
||||
}
|
||||
self.send_to_model(RequestType::Chat, window, cx);
|
||||
}
|
||||
|
||||
fn edit(&mut self, _: &Edit, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.sending_disabled(cx) {
|
||||
return;
|
||||
}
|
||||
self.send_to_model(RequestType::SuggestEdits, window, cx);
|
||||
}
|
||||
|
||||
@@ -948,7 +942,7 @@ impl ContextEditor {
|
||||
let patch_range = range.clone();
|
||||
move |cx: &mut BlockContext| {
|
||||
let max_width = cx.max_width;
|
||||
let gutter_width = cx.margins.gutter.full_width();
|
||||
let gutter_width = cx.gutter_dimensions.full_width();
|
||||
let block_id = cx.block_id;
|
||||
let selected = cx.selected;
|
||||
let window = &mut cx.window;
|
||||
@@ -1367,7 +1361,7 @@ impl ContextEditor {
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_size(TextSize::XSmall.rems())
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(colors.text_muted)
|
||||
.child("Press")
|
||||
.child(
|
||||
@@ -1494,7 +1488,7 @@ impl ContextEditor {
|
||||
|
||||
h_flex()
|
||||
.id(("message_header", message_id.as_u64()))
|
||||
.pl(cx.margins.gutter.full_width())
|
||||
.pl(cx.gutter_dimensions.full_width())
|
||||
.h_11()
|
||||
.w_full()
|
||||
.relative()
|
||||
@@ -1589,7 +1583,6 @@ impl ContextEditor {
|
||||
),
|
||||
priority: usize::MAX,
|
||||
render: render_block(MessageMetadata::from(message)),
|
||||
render_in_minimap: false,
|
||||
};
|
||||
let mut new_blocks = vec![];
|
||||
let mut block_index_to_message = vec![];
|
||||
@@ -1672,11 +1665,11 @@ impl ContextEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
|
||||
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(context_editor_view) =
|
||||
agent_panel_delegate.active_context_editor(workspace, window, cx)
|
||||
assistant_panel_delegate.active_context_editor(workspace, window, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -1702,9 +1695,9 @@ impl ContextEditor {
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let result = maybe!({
|
||||
let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
|
||||
let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
|
||||
let context_editor_view =
|
||||
agent_panel_delegate.active_context_editor(workspace, window, cx)?;
|
||||
assistant_panel_delegate.active_context_editor(workspace, window, cx)?;
|
||||
Self::get_selection_or_code_block(&context_editor_view, cx)
|
||||
});
|
||||
let Some((text, is_code_block)) = result else {
|
||||
@@ -1737,11 +1730,11 @@ impl ContextEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
|
||||
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(context_editor_view) =
|
||||
agent_panel_delegate.active_context_editor(workspace, window, cx)
|
||||
assistant_panel_delegate.active_context_editor(workspace, window, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -1827,7 +1820,7 @@ impl ContextEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
|
||||
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1858,7 +1851,7 @@ impl ContextEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
|
||||
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
|
||||
}
|
||||
|
||||
pub fn quote_ranges(
|
||||
@@ -2164,12 +2157,12 @@ impl ContextEditor {
|
||||
let image_size = size_for_image(
|
||||
&image,
|
||||
size(
|
||||
cx.max_width - cx.margins.gutter.full_width(),
|
||||
cx.max_width - cx.gutter_dimensions.full_width(),
|
||||
MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
|
||||
),
|
||||
);
|
||||
h_flex()
|
||||
.pl(cx.margins.gutter.full_width())
|
||||
.pl(cx.gutter_dimensions.full_width())
|
||||
.child(
|
||||
img(image.clone())
|
||||
.object_fit(gpui::ObjectFit::ScaleDown)
|
||||
@@ -2179,7 +2172,6 @@ impl ContextEditor {
|
||||
.into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
render_in_minimap: false,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -2444,8 +2436,17 @@ impl ContextEditor {
|
||||
None => (ButtonStyle::Filled, None),
|
||||
};
|
||||
|
||||
let model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
|
||||
let has_configuration_error = configuration_error(cx).is_some();
|
||||
let needs_to_accept_terms = self.show_accept_terms
|
||||
&& model
|
||||
.as_ref()
|
||||
.map_or(false, |model| model.provider.must_accept_terms(cx));
|
||||
let disabled = has_configuration_error || needs_to_accept_terms;
|
||||
|
||||
ButtonLike::new("send_button")
|
||||
.disabled(self.sending_disabled(cx))
|
||||
.disabled(disabled)
|
||||
.style(style)
|
||||
.when_some(tooltip, |button, tooltip| {
|
||||
button.tooltip(move |_, _| tooltip.clone())
|
||||
@@ -2467,20 +2468,6 @@ impl ContextEditor {
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether or not we should allow messages to be sent.
|
||||
/// Will return false if the selected provided has a configuration error or
|
||||
/// if the user has not accepted the terms of service for this provider.
|
||||
fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
|
||||
let model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
|
||||
let has_configuration_error = configuration_error(cx).is_some();
|
||||
let needs_to_accept_terms = self.show_accept_terms
|
||||
&& model
|
||||
.as_ref()
|
||||
.map_or(false, |model| model.provider.must_accept_terms(cx));
|
||||
has_configuration_error || needs_to_accept_terms
|
||||
}
|
||||
|
||||
fn render_edit_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
|
||||
@@ -2508,8 +2495,19 @@ impl ContextEditor {
|
||||
None => (ButtonStyle::Filled, None),
|
||||
};
|
||||
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let has_configuration_error = configuration_error(cx).is_some();
|
||||
let needs_to_accept_terms = self.show_accept_terms
|
||||
&& provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx));
|
||||
let disabled = has_configuration_error || needs_to_accept_terms;
|
||||
|
||||
ButtonLike::new("edit_button")
|
||||
.disabled(self.sending_disabled(cx))
|
||||
.disabled(disabled)
|
||||
.style(style)
|
||||
.when_some(tooltip, |button, tooltip| {
|
||||
button.tooltip(move |_, _| tooltip.clone())
|
||||
@@ -2674,7 +2672,7 @@ impl ContextEditor {
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|
||||
|this, _, _window, cx| {
|
||||
this.last_error = None;
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.open_url(&zed_urls::account_upgrade_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)))
|
||||
@@ -3361,10 +3359,10 @@ impl FollowableItem for ContextEditor {
|
||||
let editor_state = state.editor?;
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
|
||||
let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
|
||||
|
||||
let context_editor_task = workspace.update(cx, |workspace, cx| {
|
||||
agent_panel_delegate.open_remote_context(workspace, context_id, window, cx)
|
||||
assistant_panel_delegate.open_remote_context(workspace, context_id, window, cx)
|
||||
});
|
||||
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
|
||||
@@ -8,7 +8,7 @@ use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
use crate::{
|
||||
AgentPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
|
||||
AssistantPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
|
||||
@@ -70,19 +70,19 @@ impl ContextHistory {
|
||||
) {
|
||||
let SavedContextPickerEvent::Confirmed(context) = event;
|
||||
|
||||
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
|
||||
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| match context {
|
||||
ContextMetadata::Remote(metadata) => {
|
||||
agent_panel_delegate
|
||||
assistant_panel_delegate
|
||||
.open_remote_context(workspace, metadata.id.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ContextMetadata::Saved(metadata) => {
|
||||
agent_panel_delegate
|
||||
assistant_panel_delegate
|
||||
.open_saved_context(workspace, metadata.path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,11 @@ impl AssistantSettings {
|
||||
.and_then(|m| m.temperature)
|
||||
}
|
||||
|
||||
pub fn stream_edits(&self, _cx: &App) -> bool {
|
||||
// TODO: Remove the `stream_edits` setting.
|
||||
true
|
||||
}
|
||||
|
||||
pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
@@ -361,7 +366,7 @@ impl AssistantSettingsContent {
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
language_model.supports_tools(),
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task, WeakEntity};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModel;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
@@ -209,7 +208,6 @@ pub trait Tool: 'static + Send + Sync {
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult;
|
||||
|
||||
@@ -35,17 +35,25 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
const KEYS_TO_REMOVE: [&str; 5] = [
|
||||
const KEYS_TO_REMOVE: [&str; 4] = [
|
||||
"format",
|
||||
"additionalProperties",
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum",
|
||||
"optional",
|
||||
];
|
||||
for key in KEYS_TO_REMOVE {
|
||||
obj.remove(key);
|
||||
}
|
||||
|
||||
if let Some(default) = obj.get("default") {
|
||||
let is_null = default.is_null();
|
||||
// Default is not supported, so we need to remove it
|
||||
obj.remove("default");
|
||||
if is_null {
|
||||
obj.insert("nullable".to_string(), Value::Bool(true));
|
||||
}
|
||||
}
|
||||
|
||||
// If a type is not specified for an input parameter, add a default type
|
||||
if matches!(obj.get("description"), Some(Value::String(_)))
|
||||
&& !obj.contains_key("type")
|
||||
@@ -84,6 +92,26 @@ mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_transform_default_null_to_nullable() {
|
||||
let mut json = json!({
|
||||
"description": "A test field",
|
||||
"type": "string",
|
||||
"default": null
|
||||
});
|
||||
|
||||
adapt_to_json_schema_subset(&mut json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json,
|
||||
json!({
|
||||
"description": "A test field",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transform_adds_type_when_missing() {
|
||||
let mut json = json!({
|
||||
@@ -129,8 +157,7 @@ mod tests {
|
||||
"format": "uint32",
|
||||
"exclusiveMinimum": 0,
|
||||
"exclusiveMaximum": 100,
|
||||
"additionalProperties": false,
|
||||
"optional": true
|
||||
"additionalProperties": false
|
||||
});
|
||||
|
||||
adapt_to_json_schema_subset(&mut json).unwrap();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod create_file_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_agent;
|
||||
@@ -12,7 +13,9 @@ mod move_path_tool;
|
||||
mod now_tool;
|
||||
mod open_tool;
|
||||
mod read_file_tool;
|
||||
mod replace;
|
||||
mod schema;
|
||||
mod streaming_edit_file_tool;
|
||||
mod templates;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
@@ -21,12 +24,14 @@ mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use gpui::{App, Entity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use move_path_tool::MovePathTool;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
pub(crate) use templates::*;
|
||||
@@ -34,19 +39,21 @@ pub(crate) use templates::*;
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_file_tool::EditFileTool;
|
||||
use crate::fetch_tool::FetchTool;
|
||||
use crate::find_path_tool::FindPathTool;
|
||||
use crate::grep_tool::GrepTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::read_file_tool::ReadFileTool;
|
||||
use crate::streaming_edit_file_tool::StreamingEditFileTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub use edit_file_tool::EditFileToolInput;
|
||||
pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
|
||||
pub use edit_file_tool::{EditFileTool, EditFileToolInput};
|
||||
pub use find_path_tool::FindPathToolInput;
|
||||
pub use open_tool::OpenTool;
|
||||
pub use read_file_tool::ReadFileToolInput;
|
||||
pub use streaming_edit_file_tool::StreamingEditFileToolInput;
|
||||
pub use terminal_tool::TerminalTool;
|
||||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
@@ -67,7 +74,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(GrepTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
registry.register_tool(EditFileTool);
|
||||
|
||||
register_edit_file_tool(cx);
|
||||
cx.observe_global::<SettingsStore>(register_edit_file_tool)
|
||||
.detach();
|
||||
|
||||
register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
|
||||
cx.subscribe(
|
||||
@@ -94,16 +104,29 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A
|
||||
}
|
||||
}
|
||||
|
||||
fn register_edit_file_tool(cx: &mut App) {
|
||||
let registry = ToolRegistry::global(cx);
|
||||
|
||||
registry.unregister_tool(CreateFileTool);
|
||||
registry.unregister_tool(EditFileTool);
|
||||
registry.unregister_tool(StreamingEditFileTool);
|
||||
|
||||
if AssistantSettings::get_global(cx).stream_edits(cx) {
|
||||
registry.register_tool(StreamingEditFileTool);
|
||||
} else {
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(EditFileTool);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::Client;
|
||||
use clock::FakeSystemClock;
|
||||
use http_client::FakeHttpClient;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use settings::Settings;
|
||||
|
||||
#[test]
|
||||
fn test_json_schema() {
|
||||
|
||||
@@ -3,8 +3,8 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -77,7 +77,6 @@ impl Tool for CopyPathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -3,7 +3,8 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -64,7 +65,6 @@ impl Tool for CreateDirectoryTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
195
crates/assistant_tools/src/create_file_tool.rs
Normal file
195
crates/assistant_tools/src/create_file_tool.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::AnyWindowHandle;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateFileToolInput {
|
||||
/// The path where the file should be created.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following structure:
|
||||
///
|
||||
/// - directory1/
|
||||
/// - directory2/
|
||||
///
|
||||
/// You can create a new file by providing a path of "directory1/new_file.txt"
|
||||
/// </example>
|
||||
///
|
||||
/// Make sure to include this field before the `contents` field in the input object
|
||||
/// so that we can display it immediately.
|
||||
pub path: String,
|
||||
|
||||
/// The text contents of the file to create.
|
||||
///
|
||||
/// <example>
|
||||
/// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
|
||||
/// </example>
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct PartialInput {
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
contents: String,
|
||||
}
|
||||
|
||||
pub struct CreateFileTool;
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Create file";
|
||||
|
||||
impl Tool for CreateFileTool {
|
||||
fn name(&self) -> String {
|
||||
"create_file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./create_file_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileCreate
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<CreateFileToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
format!("Create file {path}")
|
||||
}
|
||||
Err(_) => DEFAULT_UI_TEXT.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<PartialInput>(input.clone()).ok() {
|
||||
Some(input) if !input.path.is_empty() => input.path,
|
||||
_ => DEFAULT_UI_TEXT.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
|
||||
Some(project_path) => project_path,
|
||||
None => {
|
||||
return Task::ready(Err(anyhow!("Path to create was outside the project"))).into();
|
||||
}
|
||||
};
|
||||
let contents: Arc<str> = input.contents.as_str().into();
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_created(buffer.clone(), cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.buffer_edited(buffer.clone(), cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created file {destination_path}").into())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_without_path() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
});
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_null() {
|
||||
let tool = CreateFileTool;
|
||||
let input = serde_json::Value::Null;
|
||||
|
||||
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_text_with_valid_input() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
|
||||
});
|
||||
|
||||
assert_eq!(tool.ui_text(&input), "Create file `src/main.rs`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_text_with_invalid_input() {
|
||||
let tool = CreateFileTool;
|
||||
let input = json!({
|
||||
"invalid": "field"
|
||||
});
|
||||
|
||||
assert_eq!(tool.ui_text(&input), DEFAULT_UI_TEXT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created.
|
||||
|
||||
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file.
|
||||
@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{Project, ProjectPath};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -62,7 +62,6 @@ impl Tool for DeletePathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{DiagnosticSeverity, OffsetRangeExt};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -82,7 +82,6 @@ impl Tool for DiagnosticsTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::*;
|
||||
use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput};
|
||||
use crate::{
|
||||
ReadFileToolInput, grep_tool::GrepToolInput,
|
||||
streaming_edit_file_tool::StreamingEditFileToolInput,
|
||||
};
|
||||
use Role::*;
|
||||
use anyhow::anyhow;
|
||||
use client::{Client, UserStore};
|
||||
@@ -66,7 +69,7 @@ fn eval_extract_handle_command_output() {
|
||||
[tool_use(
|
||||
"tool_2",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: false,
|
||||
@@ -122,7 +125,7 @@ fn eval_delete_run_git_blame() {
|
||||
[tool_use(
|
||||
"tool_2",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: false,
|
||||
@@ -237,7 +240,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
|
||||
[tool_use(
|
||||
"tool_4",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: false,
|
||||
@@ -313,7 +316,7 @@ fn eval_disable_cursor_blinking() {
|
||||
[tool_use(
|
||||
"tool_4",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: false,
|
||||
@@ -503,7 +506,7 @@ fn eval_from_pixels_constructor() {
|
||||
[tool_use(
|
||||
"tool_5",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: false,
|
||||
@@ -580,7 +583,7 @@ fn eval_zode() {
|
||||
tool_use(
|
||||
"tool_3",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: true,
|
||||
@@ -825,7 +828,7 @@ fn eval_add_overwrite_test() {
|
||||
tool_use(
|
||||
"tool_5",
|
||||
"edit_file",
|
||||
EditFileToolInput {
|
||||
StreamingEditFileToolInput {
|
||||
display_description: edit_description.into(),
|
||||
path: input_file_path.into(),
|
||||
create_or_overwrite: false,
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
use crate::{
|
||||
Templates,
|
||||
edit_agent::{EditAgent, EditAgentOutputEvent},
|
||||
replace::{replace_exact, replace_with_flexible_indent},
|
||||
schema::json_schema_for,
|
||||
streaming_edit_file_tool::StreamingEditFileToolOutput,
|
||||
};
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{
|
||||
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
|
||||
use futures::StreamExt;
|
||||
use editor::{Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, PathKey};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task,
|
||||
TextStyleRefinement, WeakEntity, pulsating_between,
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId,
|
||||
Task, TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use indoc::formatdoc;
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{AgentLocation, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -30,7 +28,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use ui::{Disclosure, Tooltip, Window, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -38,13 +36,7 @@ pub struct EditFileTool;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolInput {
|
||||
/// A one-line, user-friendly markdown description of the edit. This will be
|
||||
/// shown in the UI and also passed to another model to perform the edit.
|
||||
///
|
||||
/// Be terse, but also descriptive in what you want to achieve with this
|
||||
/// edit. Avoid generic instructions.
|
||||
///
|
||||
/// NEVER mention the file path in this description.
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
@@ -53,7 +45,7 @@ pub struct EditFileToolInput {
|
||||
/// so that we can display it immediately.
|
||||
pub display_description: String,
|
||||
|
||||
/// The full path of the file to create or modify in the project.
|
||||
/// The full path of the file to modify in the project.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
@@ -74,19 +66,11 @@ pub struct EditFileToolInput {
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// If true, this tool will recreate the file from scratch.
|
||||
/// If false, this tool will produce granular edits to an existing file.
|
||||
///
|
||||
/// When a file already exists or you just created it, always prefer editing
|
||||
/// it as opposed to recreating it from scratch.
|
||||
pub create_or_overwrite: bool,
|
||||
}
|
||||
/// The text to replace.
|
||||
pub old_string: String,
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFileToolOutput {
|
||||
pub original_path: PathBuf,
|
||||
pub new_text: String,
|
||||
pub old_text: String,
|
||||
/// The text to replace it with.
|
||||
pub new_string: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -95,6 +79,10 @@ struct PartialInput {
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
display_description: String,
|
||||
#[serde(default)]
|
||||
old_string: String,
|
||||
#[serde(default)]
|
||||
new_string: String,
|
||||
}
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
@@ -146,10 +134,9 @@ impl Tool for EditFileTool {
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
@@ -158,14 +145,6 @@ impl Tool for EditFileTool {
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Path {} not found in project",
|
||||
input.path.display()
|
||||
)))
|
||||
.into();
|
||||
};
|
||||
|
||||
let card = window.and_then(|window| {
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
@@ -177,9 +156,12 @@ impl Tool for EditFileTool {
|
||||
});
|
||||
|
||||
let card_clone = card.clone();
|
||||
let messages = messages.to_vec();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
|
||||
let task: Task<Result<ToolResultOutput, _>> = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
@@ -187,113 +169,144 @@ impl Tool for EditFileTool {
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let exists = buffer.read_with(cx, |buffer, _| {
|
||||
buffer
|
||||
.file()
|
||||
.as_ref()
|
||||
.map_or(false, |file| file.disk_state().exists())
|
||||
})?;
|
||||
if !input.create_or_overwrite && !exists {
|
||||
return Err(anyhow!("{} not found", input.path.display()));
|
||||
// Set the agent's location to the top of the file
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: language::Anchor::MIN,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.old_string.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"`old_string` can't be empty, use another tool if you want to create a file."
|
||||
));
|
||||
}
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let old_text = cx
|
||||
.background_spawn({
|
||||
let old_snapshot = old_snapshot.clone();
|
||||
async move { old_snapshot.text() }
|
||||
if input.old_string == input.new_string {
|
||||
return Err(anyhow!(
|
||||
"The `old_string` and `new_string` are identical, so no changes would be made."
|
||||
));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
// Try to match exactly
|
||||
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| {
|
||||
replace_with_flexible_indent(
|
||||
&input.old_string,
|
||||
&input.new_string,
|
||||
&snapshot,
|
||||
)
|
||||
})?;
|
||||
|
||||
if diff.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let old_text = snapshot.text();
|
||||
|
||||
Some((old_text, diff))
|
||||
})
|
||||
.await;
|
||||
|
||||
let (output, mut events) = if input.create_or_overwrite {
|
||||
edit_agent.overwrite(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
messages,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
edit_agent.edit(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
messages,
|
||||
cx,
|
||||
)
|
||||
let Some((old_text, diff)) = result else {
|
||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||
let file_exists = buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
if !file_exists {
|
||||
anyhow!("{} does not exist", input.path.display())
|
||||
} else if buffer.is_empty() {
|
||||
anyhow!(
|
||||
"{} is empty, so the provided `old_string` wasn't found.",
|
||||
input.path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow!("Failed to match the provided `old_string`")
|
||||
}
|
||||
})?;
|
||||
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
let mut hallucinated_old_text = false;
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
EditAgentOutputEvent::Edited => {
|
||||
if let Some(card) = card_clone.as_ref() {
|
||||
let new_snapshot =
|
||||
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let new_text = cx
|
||||
.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
async move { new_snapshot.text() }
|
||||
})
|
||||
.await;
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(
|
||||
project_path.path.clone(),
|
||||
old_text.clone(),
|
||||
new_text,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
|
||||
let snapshot = cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
let base_version = diff.base_version.clone();
|
||||
let snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.snapshot()
|
||||
});
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
|
||||
// Set the agent's location to the position of the first edit
|
||||
if let Some(first_edit) = snapshot.edits_since::<usize>(&base_version).next() {
|
||||
let position = snapshot.anchor_before(first_edit.new.start);
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
output.await?;
|
||||
|
||||
snapshot
|
||||
})?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await?;
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let new_text = cx.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
async move { new_snapshot.text() }
|
||||
});
|
||||
let diff = cx.background_spawn(async move {
|
||||
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
|
||||
});
|
||||
let (new_text, diff) = futures::join!(new_text, diff);
|
||||
|
||||
let output = EditFileToolOutput {
|
||||
original_path: project_path.path.to_path_buf(),
|
||||
new_text: new_text.clone(),
|
||||
old_text: old_text.clone(),
|
||||
};
|
||||
let new_text = snapshot.text();
|
||||
let diff_str = cx
|
||||
.background_spawn({
|
||||
let old_text = old_text.clone();
|
||||
let new_text = new_text.clone();
|
||||
async move { language::unified_diff(&old_text, &new_text) }
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
card.set_diff(
|
||||
project_path.path.clone(),
|
||||
old_text.clone(),
|
||||
new_text.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let input_path = input.path.display();
|
||||
if diff.is_empty() {
|
||||
if hallucinated_old_text {
|
||||
Err(anyhow!(formatdoc! {"
|
||||
Some edits were produced but none of them could be applied.
|
||||
Read the relevant sections of {input_path} again so that
|
||||
I can perform the requested edits.
|
||||
"}))
|
||||
} else {
|
||||
Ok("No edits were made.".to_string().into())
|
||||
}
|
||||
} else {
|
||||
Ok(ToolResultOutput {
|
||||
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
|
||||
output: serde_json::to_value(output).ok(),
|
||||
Ok(ToolResultOutput {
|
||||
content: format!(
|
||||
"Edited {}:\n\n```diff\n{}\n```",
|
||||
input.path.display(),
|
||||
diff_str
|
||||
),
|
||||
output: serde_json::to_value(StreamingEditFileToolOutput {
|
||||
original_path: input.path,
|
||||
new_text,
|
||||
old_text,
|
||||
})
|
||||
}
|
||||
.ok(),
|
||||
})
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
@@ -309,7 +322,7 @@ impl Tool for EditFileTool {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyToolCard> {
|
||||
let output = match serde_json::from_value::<EditFileToolOutput>(output) {
|
||||
let output = match serde_json::from_value::<StreamingEditFileToolOutput>(output) {
|
||||
Ok(output) => output,
|
||||
Err(_) => return None,
|
||||
};
|
||||
@@ -360,9 +373,9 @@ impl EditFileToolCard {
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.disable_expand_excerpt_buttons(cx);
|
||||
editor.disable_scrollbars_and_minimap(cx);
|
||||
editor.set_soft_wrap_mode(SoftWrap::None, cx);
|
||||
editor.scroll_manager.set_forbid_vertical_scroll(true);
|
||||
editor.set_show_scrollbars(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_breakpoints(false, cx);
|
||||
@@ -574,16 +587,33 @@ impl ToolCard for EditFileToolCard {
|
||||
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
|
||||
.unwrap_or_default();
|
||||
|
||||
editor.set_text_style_refinement(TextStyleRefinement {
|
||||
font_size: Some(
|
||||
TextSize::Small
|
||||
.rems()
|
||||
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
|
||||
.into(),
|
||||
),
|
||||
..TextStyleRefinement::default()
|
||||
});
|
||||
let element = editor.render(window, cx);
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let element = EditorElement::new(
|
||||
&cx.entity(),
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
horizontal_padding: rems(0.25).to_pixels(window.rem_size()),
|
||||
local_player: cx.theme().players().local(),
|
||||
text: TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: TextSize::Small
|
||||
.rems(cx)
|
||||
.to_pixels(settings.agent_font_size(cx))
|
||||
.into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
},
|
||||
scrollbar_width: EditorElement::SCROLLBAR_WIDTH,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
status: cx.theme().status().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
(element.into_any_element(), line_height)
|
||||
});
|
||||
|
||||
@@ -682,7 +712,7 @@ impl ToolCard for EditFileToolCard {
|
||||
.child(
|
||||
div()
|
||||
.rounded_md()
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.children(
|
||||
error_message
|
||||
@@ -822,40 +852,7 @@ async fn build_buffer_diff(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = serde_json::to_value(EditFileToolInput {
|
||||
display_description: "Some edit".into(),
|
||||
path: "root/nonexistent_file.txt".into(),
|
||||
create_or_overwrite: false,
|
||||
})
|
||||
.unwrap();
|
||||
Arc::new(EditFileTool)
|
||||
.run(input, &[], project.clone(), action_log, model, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"root/nonexistent_file.txt not found"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
@@ -923,13 +920,4 @@ mod tests {
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
|
||||
This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files.
|
||||
|
||||
Before using this tool:
|
||||
|
||||
@@ -6,3 +6,40 @@ Before using this tool:
|
||||
|
||||
2. Verify the directory path is correct (only applicable when creating new files):
|
||||
- Use the `list_directory` tool to verify the parent directory exists and is the correct location
|
||||
|
||||
To make a file edit, provide the following:
|
||||
1. path: The full path to the file you wish to modify in the project. This path must include the root directory in the project.
|
||||
2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
|
||||
3. new_string: The edited text, which will replace the old_string in the file.
|
||||
|
||||
The tool will replace ONE occurrence of old_string with new_string in the specified file.
|
||||
|
||||
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
|
||||
|
||||
1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
|
||||
- Include AT LEAST 3-5 lines of context BEFORE the change point
|
||||
- Include AT LEAST 3-5 lines of context AFTER the change point
|
||||
- Include all whitespace, indentation, and surrounding code exactly as it appears in the file
|
||||
|
||||
2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
|
||||
- Make separate calls to this tool for each instance
|
||||
- Each call must uniquely identify its specific instance using extensive context
|
||||
|
||||
3. VERIFICATION: Before using this tool:
|
||||
- Check how many instances of the target text exist in the file
|
||||
- If multiple instances exist, gather enough context to uniquely identify each one
|
||||
- Plan separate tool calls for each instance
|
||||
|
||||
WARNING: If you do not follow these requirements:
|
||||
- The tool will fail if old_string matches multiple locations
|
||||
- The tool will fail if old_string doesn't match exactly (including whitespace)
|
||||
- You may change the wrong instance if you don't include enough context
|
||||
|
||||
When making edits:
|
||||
- Ensure the edit results in idiomatic, correct code
|
||||
- Do not leave the code in a broken state
|
||||
- Always use fully-qualified project paths (starting with the name of one of the project's root directories)
|
||||
|
||||
If you want to create a new file, use the `create_file` tool instead of this tool. Don't pass an empty `old_string`.
|
||||
|
||||
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.
|
||||
|
||||
@@ -9,7 +9,7 @@ use futures::AsyncReadExt as _;
|
||||
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -145,7 +145,6 @@ impl Tool for FetchTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
};
|
||||
use language;
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -76,7 +76,6 @@ impl Tool for FindPathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language::{OffsetRangeExt, ParseStatus, Point};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{
|
||||
Project,
|
||||
search::{SearchQuery, SearchResult},
|
||||
@@ -99,7 +99,6 @@ impl Tool for GrepTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
@@ -282,7 +281,6 @@ mod tests {
|
||||
use assistant_tool::Tool;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use settings::SettingsStore;
|
||||
use unindent::Unindent;
|
||||
@@ -745,8 +743,7 @@ mod tests {
|
||||
) -> String {
|
||||
let tool = Arc::new(GrepTool);
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, model, None, cx));
|
||||
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
|
||||
|
||||
match task.output.await {
|
||||
Ok(result) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -76,7 +76,6 @@ impl Tool for ListDirectoryTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -89,7 +89,6 @@ impl Tool for MovePathTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -5,7 +5,7 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use chrono::{Local, Utc};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -59,7 +59,6 @@ impl Tool for NowTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -52,7 +52,6 @@ impl Tool for OpenTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use indoc::formatdoc;
|
||||
use itertools::Itertools;
|
||||
use language::{Anchor, Point};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::{AgentLocation, Project};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -86,7 +86,6 @@ impl Tool for ReadFileTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
@@ -98,22 +97,27 @@ impl Tool for ReadFileTool {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
|
||||
};
|
||||
let exists = worktree.update(cx, |worktree, cx| {
|
||||
worktree.file_exists(&project_path.path, cx)
|
||||
});
|
||||
|
||||
let file_path = input.path.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
if !exists.await? {
|
||||
return Err(anyhow!("{} not found", file_path));
|
||||
}
|
||||
|
||||
let buffer = cx
|
||||
.update(|cx| {
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
|
||||
})?
|
||||
.await?;
|
||||
if buffer.read_with(cx, |buffer, _| {
|
||||
buffer
|
||||
.file()
|
||||
.as_ref()
|
||||
.map_or(true, |file| !file.disk_state().exists())
|
||||
})? {
|
||||
return Err(anyhow!("{} not found", file_path));
|
||||
}
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_agent_location(
|
||||
@@ -141,13 +145,9 @@ impl Tool for ReadFileTool {
|
||||
let lines = text.split('\n').skip(start_row as usize);
|
||||
if let Some(end) = input.end_line {
|
||||
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
|
||||
Itertools::intersperse(lines.take(count as usize), "\n")
|
||||
.collect::<String>()
|
||||
.into()
|
||||
Itertools::intersperse(lines.take(count as usize), "\n").collect::<String>().into()
|
||||
} else {
|
||||
Itertools::intersperse(lines, "\n")
|
||||
.collect::<String>()
|
||||
.into()
|
||||
Itertools::intersperse(lines, "\n").collect::<String>().into()
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -184,20 +184,15 @@ impl Tool for ReadFileTool {
|
||||
} else {
|
||||
// File is too big, so return the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
let outline =
|
||||
outline::file_outline(project, file_path, action_log, None, cx).await?;
|
||||
let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once.
|
||||
|
||||
Here is an outline of its symbols:
|
||||
This file was too big to read all at once. Here is an outline of its symbols:
|
||||
|
||||
{outline}
|
||||
|
||||
Using the line numbers in this outline, you can call this tool again
|
||||
while specifying the start_line and end_line fields to see the
|
||||
implementations of symbols in the outline."
|
||||
}
|
||||
.into())
|
||||
Using the line numbers in this outline, you can call this tool again while specifying
|
||||
the start_line and end_line fields to see the implementations of symbols in the outline."
|
||||
}.into())
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -210,7 +205,6 @@ mod test {
|
||||
use super::*;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Language, LanguageConfig, LanguageMatcher};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
@@ -224,14 +218,13 @@ mod test {
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/nonexistent_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, model, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -255,14 +248,13 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
"path": "root/small_file.txt"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, model, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -285,7 +277,6 @@ mod test {
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(rust_lang()));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
@@ -293,21 +284,13 @@ mod test {
|
||||
"path": "root/large_file.rs"
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(
|
||||
input,
|
||||
&[],
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.run(input, &[], project.clone(), action_log.clone(), None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
let content = result.unwrap();
|
||||
assert_eq!(
|
||||
content.lines().skip(4).take(6).collect::<Vec<_>>(),
|
||||
content.lines().skip(2).take(6).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"struct Test0 [L1-4]",
|
||||
" a [L2]",
|
||||
@@ -325,7 +308,7 @@ mod test {
|
||||
"offset": 1
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, model, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -342,7 +325,7 @@ mod test {
|
||||
pretty_assertions::assert_eq!(
|
||||
content
|
||||
.lines()
|
||||
.skip(4)
|
||||
.skip(2)
|
||||
.take(expected_content.len())
|
||||
.collect::<Vec<_>>(),
|
||||
expected_content
|
||||
@@ -363,7 +346,6 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let result = cx
|
||||
.update(|cx| {
|
||||
let input = json!({
|
||||
@@ -372,7 +354,7 @@ mod test {
|
||||
"end_line": 4
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, model, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -393,7 +375,6 @@ mod test {
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
|
||||
// start_line of 0 should be treated as 1
|
||||
let result = cx
|
||||
@@ -404,15 +385,7 @@ mod test {
|
||||
"end_line": 2
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(
|
||||
input,
|
||||
&[],
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.run(input, &[], project.clone(), action_log.clone(), None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -427,15 +400,7 @@ mod test {
|
||||
"end_line": 0
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(
|
||||
input,
|
||||
&[],
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.run(input, &[], project.clone(), action_log.clone(), None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
@@ -450,7 +415,7 @@ mod test {
|
||||
"end_line": 2
|
||||
});
|
||||
Arc::new(ReadFileTool)
|
||||
.run(input, &[], project.clone(), action_log, model, None, cx)
|
||||
.run(input, &[], project.clone(), action_log, None, cx)
|
||||
.output
|
||||
})
|
||||
.await;
|
||||
|
||||
872
crates/assistant_tools/src/replace.rs
Normal file
872
crates/assistant_tools/src/replace.rs
Normal file
@@ -0,0 +1,872 @@
|
||||
use language::{BufferSnapshot, Diff, Point, ToOffset};
|
||||
use project::search::SearchQuery;
|
||||
use std::iter;
|
||||
use util::{ResultExt as _, paths::PathMatcher};
|
||||
|
||||
/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching.
|
||||
/// Uses the search functionality to locate the first occurrence of the exact string.
|
||||
/// Returns None if no exact match is found in the buffer.
|
||||
pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> Option<Diff> {
|
||||
let query = SearchQuery::text(
|
||||
old,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
PathMatcher::new(iter::empty::<&str>()).ok()?,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.log_err()?;
|
||||
|
||||
let matches = query.search(&snapshot, None).await;
|
||||
|
||||
if matches.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let edit_range = matches[0].clone();
|
||||
let diff = language::text_diff(&old, &new);
|
||||
|
||||
let edits = diff
|
||||
.into_iter()
|
||||
.map(|(old_range, text)| {
|
||||
let start = edit_range.start + old_range.start;
|
||||
let end = edit_range.start + old_range.end;
|
||||
(start..end, text)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let diff = language::Diff {
|
||||
base_version: snapshot.version().clone(),
|
||||
line_ending: snapshot.line_ending(),
|
||||
edits,
|
||||
};
|
||||
|
||||
Some(diff)
|
||||
}
|
||||
|
||||
/// Performs a replacement that's indentation-aware - matches text content ignoring leading whitespace differences.
|
||||
/// When replacing, preserves the indentation level found in the buffer at each matching line.
|
||||
/// Returns None if no match found or if indentation is offset inconsistently across matched lines.
|
||||
pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapshot) -> Option<Diff> {
|
||||
let (old_lines, old_min_indent) = lines_with_min_indent(old);
|
||||
let (new_lines, new_min_indent) = lines_with_min_indent(new);
|
||||
let min_indent = old_min_indent.min(new_min_indent);
|
||||
|
||||
let old_lines = drop_lines_prefix(&old_lines, min_indent);
|
||||
let new_lines = drop_lines_prefix(&new_lines, min_indent);
|
||||
|
||||
let max_row = buffer.max_point().row;
|
||||
|
||||
'windows: for start_row in 0..max_row + 1 {
|
||||
let end_row = start_row + old_lines.len().saturating_sub(1) as u32;
|
||||
|
||||
if end_row > max_row {
|
||||
// The buffer ends before fully matching the pattern
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_point = Point::new(start_row, 0);
|
||||
let end_point = Point::new(end_row, buffer.line_len(end_row));
|
||||
let range = start_point.to_offset(buffer)..end_point.to_offset(buffer);
|
||||
|
||||
let window_text = buffer.text_for_range(range.clone());
|
||||
let mut window_lines = window_text.lines();
|
||||
let mut old_lines_iter = old_lines.iter();
|
||||
|
||||
let mut common_mismatch = None;
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum Mismatch {
|
||||
OverIndented(String),
|
||||
UnderIndented(String),
|
||||
}
|
||||
|
||||
while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
|
||||
{
|
||||
let line_trimmed = window_line.trim_start();
|
||||
|
||||
if line_trimmed != old_line.trim_start() {
|
||||
continue 'windows;
|
||||
}
|
||||
|
||||
if line_trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let line_mismatch = if window_line.len() > old_line.len() {
|
||||
let prefix = window_line[..window_line.len() - old_line.len()].to_string();
|
||||
Mismatch::UnderIndented(prefix)
|
||||
} else {
|
||||
let prefix = old_line[..old_line.len() - window_line.len()].to_string();
|
||||
Mismatch::OverIndented(prefix)
|
||||
};
|
||||
|
||||
match &common_mismatch {
|
||||
Some(common_mismatch) if common_mismatch != &line_mismatch => {
|
||||
continue 'windows;
|
||||
}
|
||||
Some(_) => (),
|
||||
None => common_mismatch = Some(line_mismatch),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(common_mismatch) = &common_mismatch {
|
||||
let line_ending = buffer.line_ending();
|
||||
let replacement = new_lines
|
||||
.iter()
|
||||
.map(|new_line| {
|
||||
if new_line.trim().is_empty() {
|
||||
new_line.to_string()
|
||||
} else {
|
||||
match common_mismatch {
|
||||
Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,
|
||||
Mismatch::OverIndented(prefix) => new_line
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or(new_line)
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(line_ending.as_str());
|
||||
|
||||
let diff = Diff {
|
||||
base_version: buffer.version().clone(),
|
||||
line_ending,
|
||||
edits: vec![(range, replacement.into())],
|
||||
};
|
||||
|
||||
return Some(diff);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> {
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| line.get(prefix_len..).unwrap_or(""))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
|
||||
let mut lines = Vec::new();
|
||||
let mut min_indent: Option<usize> = None;
|
||||
|
||||
for line in input.lines() {
|
||||
lines.push(line);
|
||||
if !line.trim().is_empty() {
|
||||
let indent = line.len() - line.trim_start().len();
|
||||
min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
|
||||
}
|
||||
}
|
||||
|
||||
(lines, min_indent.unwrap_or(0))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod replace_exact_tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::prelude::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn basic(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, Some("let x = 42;".to_string()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn no_match(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn multi_line(cx: &mut TestAppContext) {
|
||||
let whole = "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}";
|
||||
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
|
||||
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
|
||||
let result = test_replace_exact(cx, whole, old_text, new_text).await;
|
||||
assert_eq!(
|
||||
result,
|
||||
Some("fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn multiple_occurrences(cx: &mut TestAppContext) {
|
||||
let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";
|
||||
let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(
|
||||
result,
|
||||
Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn empty_buffer(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn partial_match(cx: &mut TestAppContext) {
|
||||
let whole = "let x = 41; let y = 42;";
|
||||
let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;
|
||||
assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn whitespace_sensitive(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn entire_buffer(cx: &mut TestAppContext) {
|
||||
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
|
||||
assert_eq!(result, Some("let x = 42;".to_string()));
|
||||
}
|
||||
|
||||
async fn test_replace_exact(
|
||||
cx: &mut TestAppContext,
|
||||
whole: &str,
|
||||
old: &str,
|
||||
new: &str,
|
||||
) -> Option<String> {
|
||||
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
|
||||
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact(old, new, &buffer_snapshot).await;
|
||||
diff.map(|diff| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod flexible_indent_tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::prelude::*;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_underindented_single_line(cx: &mut TestAppContext) {
|
||||
let cur = " let a = 41;".to_string();
|
||||
let old = " let a = 41;".to_string();
|
||||
let new = " let a = 42;".to_string();
|
||||
let exp = " let a = 42;".to_string();
|
||||
|
||||
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
|
||||
|
||||
assert_eq!(result, Some(exp.to_string()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_overindented_single_line(cx: &mut TestAppContext) {
|
||||
let cur = " let a = 41;".to_string();
|
||||
let old = " let a = 41;".to_string();
|
||||
let new = " let a = 42;".to_string();
|
||||
let exp = " let a = 42;".to_string();
|
||||
|
||||
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
|
||||
|
||||
assert_eq!(result, Some(exp.to_string()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_underindented_multi_line(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
println!("x = {}", x);
|
||||
let y = 10;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
println!("x = {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 42;
|
||||
println!("New value: {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let expected = r#"
|
||||
fn test() {
|
||||
let x = 42;
|
||||
println!("New value: {}", x);
|
||||
let y = 10;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
Some(expected.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_overindented_multi_line(cx: &mut TestAppContext) {
|
||||
let cur = r#"
|
||||
fn foo() {
|
||||
let a = 41;
|
||||
let b = 3.13;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
// 6 space indent instead of 4
|
||||
let old = " let a = 41;\n let b = 3.13;";
|
||||
let new = " let a = 42;\n let b = 3.14;";
|
||||
|
||||
let expected = r#"
|
||||
fn foo() {
|
||||
let a = 42;
|
||||
let b = 3.14;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
|
||||
|
||||
assert_eq!(result, Some(expected.to_string()))
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
if condition {
|
||||
println!("{}", 43);
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
if condition {
|
||||
println!("{}", 43);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
if condition {
|
||||
println!("{}", 42);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_with_empty_lines(cx: &mut TestAppContext) {
|
||||
// Test with empty lines
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
|
||||
println!("x = {}", x);
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
|
||||
println!("x = {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 10;
|
||||
|
||||
println!("New x: {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let expected = r#"
|
||||
fn test() {
|
||||
let x = 10;
|
||||
|
||||
println!("New x: {}", x);
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
Some(expected.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_no_match(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let y = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let y = 20;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
println!("x = {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 10;
|
||||
println!("x = {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
// Should return None because whole doesn't fully contain the old text
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
let x = 5;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
let y = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 5;
|
||||
let y = 20;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_old_is_empty(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
let x = 5;
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = "";
|
||||
let new = r#"
|
||||
let y = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
|
||||
let whole = "";
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 10;
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lines_with_min_indent() {
|
||||
// Empty string
|
||||
assert_eq!(lines_with_min_indent(""), (vec![], 0));
|
||||
|
||||
// Single line without indentation
|
||||
assert_eq!(lines_with_min_indent("hello"), (vec!["hello"], 0));
|
||||
|
||||
// Multiple lines with no indentation
|
||||
assert_eq!(
|
||||
lines_with_min_indent("line1\nline2\nline3"),
|
||||
(vec!["line1", "line2", "line3"], 0)
|
||||
);
|
||||
|
||||
// Multiple lines with consistent indentation
|
||||
assert_eq!(
|
||||
lines_with_min_indent(" line1\n line2\n line3"),
|
||||
(vec![" line1", " line2", " line3"], 2)
|
||||
);
|
||||
|
||||
// Multiple lines with varying indentation
|
||||
assert_eq!(
|
||||
lines_with_min_indent(" line1\n line2\n line3"),
|
||||
(vec![" line1", " line2", " line3"], 2)
|
||||
);
|
||||
|
||||
// Lines with mixed indentation and empty lines
|
||||
assert_eq!(
|
||||
lines_with_min_indent(" line1\n\n line2"),
|
||||
(vec![" line1", "", " line2"], 2)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_with_missing_indent_uneven_match(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
fn test() {
|
||||
if true {
|
||||
let x = 5;
|
||||
println!("x = {}", x);
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
let x = 5;
|
||||
println!("x = {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
let x = 42;
|
||||
println!("x = {}", x);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let expected = r#"
|
||||
fn test() {
|
||||
if true {
|
||||
let x = 42;
|
||||
println!("x = {}", x);
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
Some(expected.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_replace_big_example(cx: &mut TestAppContext) {
|
||||
let whole = r#"
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_age() {
|
||||
assert!(is_valid_age(0));
|
||||
assert!(!is_valid_age(151));
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let old = r#"
|
||||
#[test]
|
||||
fn test_is_valid_age() {
|
||||
assert!(is_valid_age(0));
|
||||
assert!(!is_valid_age(151));
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new = r#"
|
||||
#[test]
|
||||
fn test_is_valid_age() {
|
||||
assert!(is_valid_age(0));
|
||||
assert!(!is_valid_age(151));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_people_by_age() {
|
||||
let people = vec![
|
||||
Person::new("Young One", 5, "young@example.com").unwrap(),
|
||||
Person::new("Teen One", 15, "teen@example.com").unwrap(),
|
||||
Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
|
||||
Person::new("Adult One", 25, "adult@example.com").unwrap(),
|
||||
];
|
||||
|
||||
let groups = group_people_by_age(&people);
|
||||
|
||||
assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9
|
||||
assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
|
||||
assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
let expected = r#"
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_age() {
|
||||
assert!(is_valid_age(0));
|
||||
assert!(!is_valid_age(151));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_people_by_age() {
|
||||
let people = vec![
|
||||
Person::new("Young One", 5, "young@example.com").unwrap(),
|
||||
Person::new("Teen One", 15, "teen@example.com").unwrap(),
|
||||
Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
|
||||
Person::new("Adult One", 25, "adult@example.com").unwrap(),
|
||||
];
|
||||
|
||||
let groups = group_people_by_age(&people);
|
||||
|
||||
assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9
|
||||
assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
|
||||
assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
assert_eq!(
|
||||
test_replace_with_flexible_indent(cx, &whole, &old, &new),
|
||||
Some(expected.to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drop_lines_prefix() {
|
||||
// Empty array
|
||||
assert_eq!(drop_lines_prefix(&[], 2), Vec::<&str>::new());
|
||||
|
||||
// Zero prefix length
|
||||
assert_eq!(
|
||||
drop_lines_prefix(&["line1", "line2"], 0),
|
||||
vec!["line1", "line2"]
|
||||
);
|
||||
|
||||
// Normal prefix drop
|
||||
assert_eq!(
|
||||
drop_lines_prefix(&[" line1", " line2"], 2),
|
||||
vec!["line1", "line2"]
|
||||
);
|
||||
|
||||
// Prefix longer than some lines
|
||||
assert_eq!(drop_lines_prefix(&[" line1", "a"], 2), vec!["line1", ""]);
|
||||
|
||||
// Prefix longer than all lines
|
||||
assert_eq!(drop_lines_prefix(&["a", "b"], 5), vec!["", ""]);
|
||||
|
||||
// Mixed length lines
|
||||
assert_eq!(
|
||||
drop_lines_prefix(&[" line1", " line2", " line3"], 2),
|
||||
vec![" line1", "line2", " line3"]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_basic(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
assert_eq!(diff.edits.len(), 1);
|
||||
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42;");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
|
||||
assert!(diff.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| {
|
||||
language::Buffer::local(
|
||||
"fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}",
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
|
||||
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
|
||||
let diff = replace_exact(old_text, new_text, &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
"fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
|
||||
let buffer =
|
||||
cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
// Should replace only the first occurrence
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
// Verify substring replacement actually works
|
||||
let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42; let y = 42;");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
|
||||
assert!(diff.is_some());
|
||||
|
||||
let diff = diff.unwrap();
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
});
|
||||
|
||||
assert_eq!(result, "let x = 42;");
|
||||
}
|
||||
|
||||
fn test_replace_with_flexible_indent(
|
||||
cx: &mut TestAppContext,
|
||||
whole: &str,
|
||||
old: &str,
|
||||
new: &str,
|
||||
) -> Option<String> {
|
||||
// Create a local buffer with the test content
|
||||
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
|
||||
|
||||
// Get the buffer snapshot
|
||||
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
|
||||
|
||||
// Call replace_flexible and transform the result
|
||||
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let _ = buffer.apply_diff(diff, cx);
|
||||
buffer.text()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
397
crates/assistant_tools/src/streaming_edit_file_tool.rs
Normal file
397
crates/assistant_tools/src/streaming_edit_file_tool.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
use crate::{
|
||||
Templates,
|
||||
edit_agent::{EditAgent, EditAgentOutputEvent},
|
||||
edit_file_tool::EditFileToolCard,
|
||||
schema::json_schema_for,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult, ToolResultOutput};
|
||||
use futures::StreamExt;
|
||||
use gpui::{AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task};
|
||||
use indoc::formatdoc;
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolSchemaFormat,
|
||||
};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct StreamingEditFileTool;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StreamingEditFileToolInput {
|
||||
/// A one-line, user-friendly markdown description of the edit. This will be
|
||||
/// shown in the UI and also passed to another model to perform the edit.
|
||||
///
|
||||
/// Be terse, but also descriptive in what you want to achieve with this
|
||||
/// edit. Avoid generic instructions.
|
||||
///
|
||||
/// NEVER mention the file path in this description.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
///
|
||||
/// Make sure to include this field before all the others in the input object
|
||||
/// so that we can display it immediately.
|
||||
pub display_description: String,
|
||||
|
||||
/// The full path of the file to create or modify in the project.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - backend
|
||||
/// - frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// `frontend/db.js`
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// If true, this tool will recreate the file from scratch.
|
||||
/// If false, this tool will produce granular edits to an existing file.
|
||||
///
|
||||
/// When a file already exists or you just created it, always prefer editing
|
||||
/// it as opposed to recreating it from scratch.
|
||||
pub create_or_overwrite: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct StreamingEditFileToolOutput {
|
||||
pub original_path: PathBuf,
|
||||
pub new_text: String,
|
||||
pub old_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct PartialInput {
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
#[serde(default)]
|
||||
display_description: String,
|
||||
}
|
||||
|
||||
const DEFAULT_UI_TEXT: &str = "Editing file";
|
||||
|
||||
impl Tool for StreamingEditFileTool {
|
||||
fn name(&self) -> String {
|
||||
"edit_file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("streaming_edit_file_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<StreamingEditFileToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<StreamingEditFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Editing file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
|
||||
if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
|
||||
let description = input.display_description.trim();
|
||||
if !description.is_empty() {
|
||||
return description.to_string();
|
||||
}
|
||||
|
||||
let path = input.path.trim();
|
||||
if !path.is_empty() {
|
||||
return path.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_UI_TEXT.to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<StreamingEditFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
|
||||
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Path {} not found in project",
|
||||
input.path.display()
|
||||
)))
|
||||
.into();
|
||||
};
|
||||
let Some(worktree) = project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
|
||||
};
|
||||
let exists = worktree.update(cx, |worktree, cx| {
|
||||
worktree.file_exists(&project_path.path, cx)
|
||||
});
|
||||
|
||||
let card = window.and_then(|window| {
|
||||
window
|
||||
.update(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let card_clone = card.clone();
|
||||
let messages = messages.to_vec();
|
||||
let task = cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
if !input.create_or_overwrite && !exists.await? {
|
||||
return Err(anyhow!("{} not found", input.path.display()));
|
||||
}
|
||||
|
||||
let model = cx
|
||||
.update(|cx| LanguageModelRegistry::read_global(cx).default_model())?
|
||||
.context("default model not set")?
|
||||
.model;
|
||||
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let old_text = cx
|
||||
.background_spawn({
|
||||
let old_snapshot = old_snapshot.clone();
|
||||
async move { old_snapshot.text() }
|
||||
})
|
||||
.await;
|
||||
|
||||
let (output, mut events) = if input.create_or_overwrite {
|
||||
edit_agent.overwrite(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
messages,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
edit_agent.edit(
|
||||
buffer.clone(),
|
||||
input.display_description.clone(),
|
||||
messages,
|
||||
cx,
|
||||
)
|
||||
};
|
||||
|
||||
let mut hallucinated_old_text = false;
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
EditAgentOutputEvent::Edited => {
|
||||
if let Some(card) = card_clone.as_ref() {
|
||||
let new_snapshot =
|
||||
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let new_text = cx
|
||||
.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
async move { new_snapshot.text() }
|
||||
})
|
||||
.await;
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(
|
||||
project_path.path.clone(),
|
||||
old_text.clone(),
|
||||
new_text,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
|
||||
}
|
||||
}
|
||||
output.await?;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let new_text = cx.background_spawn({
|
||||
let new_snapshot = new_snapshot.clone();
|
||||
async move { new_snapshot.text() }
|
||||
});
|
||||
let diff = cx.background_spawn(async move {
|
||||
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
|
||||
});
|
||||
let (new_text, diff) = futures::join!(new_text, diff);
|
||||
|
||||
let output = StreamingEditFileToolOutput {
|
||||
original_path: project_path.path.to_path_buf(),
|
||||
new_text: new_text.clone(),
|
||||
old_text: old_text.clone(),
|
||||
};
|
||||
|
||||
if let Some(card) = card_clone {
|
||||
card.update(cx, |card, cx| {
|
||||
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let input_path = input.path.display();
|
||||
if diff.is_empty() {
|
||||
if hallucinated_old_text {
|
||||
Err(anyhow!(formatdoc! {"
|
||||
Some edits were produced but none of them could be applied.
|
||||
Read the relevant sections of {input_path} again so that
|
||||
I can perform the requested edits.
|
||||
"}))
|
||||
} else {
|
||||
Ok("No edits were made.".to_string().into())
|
||||
}
|
||||
} else {
|
||||
Ok(ToolResultOutput {
|
||||
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
|
||||
output: serde_json::to_value(output).ok(),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
output: task,
|
||||
card: card.map(AnyToolCard::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_card(
|
||||
self: Arc<Self>,
|
||||
output: serde_json::Value,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyToolCard> {
|
||||
let output = match serde_json::from_value::<StreamingEditFileToolOutput>(output) {
|
||||
Ok(output) => output,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let card = cx.new(|cx| {
|
||||
let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
|
||||
card.set_diff(
|
||||
output.original_path.into(),
|
||||
output.old_text,
|
||||
output.new_text,
|
||||
cx,
|
||||
);
|
||||
card
|
||||
});
|
||||
|
||||
Some(card.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path() {
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
StreamingEditFileTool.still_streaming_ui_text(&input),
|
||||
"src/main.rs"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_description() {
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
StreamingEditFileTool.still_streaming_ui_text(&input),
|
||||
"Fix error handling",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_path_and_description() {
|
||||
let input = json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
StreamingEditFileTool.still_streaming_ui_text(&input),
|
||||
"Fix error handling",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_no_path_or_description() {
|
||||
let input = json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
StreamingEditFileTool.still_streaming_ui_text(&input),
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_streaming_ui_text_with_null() {
|
||||
let input = serde_json::Value::Null;
|
||||
|
||||
assert_eq!(
|
||||
StreamingEditFileTool.still_streaming_ui_text(&input),
|
||||
DEFAULT_UI_TEXT,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
|
||||
|
||||
Before using this tool:
|
||||
|
||||
1. Use the `read_file` tool to understand the file's contents and context
|
||||
|
||||
2. Verify the directory path is correct (only applicable when creating new files):
|
||||
- Use the `list_directory` tool to verify the parent directory exists and is the correct location
|
||||
@@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, WeakEntity, Window};
|
||||
use language::LineEnding;
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use project::{Project, terminals::TerminalKind};
|
||||
use schemars::JsonSchema;
|
||||
@@ -110,7 +110,6 @@ impl Tool for TerminalTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
@@ -586,7 +585,7 @@ impl ToolCard for TerminalToolCard {
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.child(terminal.clone()),
|
||||
)
|
||||
})
|
||||
@@ -599,7 +598,6 @@ mod tests {
|
||||
use editor::EditorSettings;
|
||||
use fs::RealFs;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -641,7 +639,6 @@ mod tests {
|
||||
let project: Entity<Project> =
|
||||
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
|
||||
let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
|
||||
let input = TerminalToolInput {
|
||||
command: "cat".to_owned(),
|
||||
@@ -659,7 +656,6 @@ mod tests {
|
||||
&[],
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -685,7 +681,6 @@ mod tests {
|
||||
let project: Entity<Project> =
|
||||
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
|
||||
let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
|
||||
let check = |input, expected, cx: &mut App| {
|
||||
let headless_result = TerminalTool::run(
|
||||
@@ -694,7 +689,6 @@ mod tests {
|
||||
&[],
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
model.clone(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolResult};
|
||||
use gpui::{AnyWindowHandle, App, Entity, Task};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -50,7 +50,6 @@ impl Tool for ThinkingTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
_cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -8,7 +8,7 @@ use futures::{Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -56,7 +56,6 @@ impl Tool for WebSearchTool {
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
|
||||
@@ -335,13 +335,9 @@ impl AutoUpdater {
|
||||
self.status.clone()
|
||||
}
|
||||
|
||||
pub fn dismiss_error(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
if self.status == AutoUpdateStatus::Idle {
|
||||
return false;
|
||||
}
|
||||
pub fn dismiss_error(&mut self, cx: &mut Context<Self>) {
|
||||
self.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
true
|
||||
}
|
||||
|
||||
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
|
||||
|
||||
@@ -44,7 +44,7 @@ impl Render for Breadcrumbs {
|
||||
.id("breadcrumb-container")
|
||||
.flex_grow()
|
||||
.overflow_x_scroll()
|
||||
.text_ui();
|
||||
.text_ui(cx);
|
||||
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
return element;
|
||||
|
||||
@@ -17,3 +17,8 @@ fn server_url(cx: &App) -> &str {
|
||||
pub fn account_url(cx: &App) -> String {
|
||||
format!("{server_url}/account", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
/// Returns the URL to the page to upgrade a trial or start a new zed pro subscription.
|
||||
pub fn account_upgrade_url(cx: &App) -> String {
|
||||
format!("{server_url}/account/upgrade", server_url = server_url(cx))
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
|
||||
@@ -71,7 +71,6 @@ struct GetBillingPreferencesParams {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct BillingPreferencesResponse {
|
||||
trial_started_at: Option<String>,
|
||||
max_monthly_llm_usage_spending_in_cents: i32,
|
||||
model_request_overages_enabled: bool,
|
||||
model_request_overages_spend_limit_in_cents: i32,
|
||||
@@ -87,17 +86,9 @@ async fn get_billing_preferences(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
|
||||
let preferences = app.db.get_billing_preferences(user.id).await?;
|
||||
|
||||
Ok(Json(BillingPreferencesResponse {
|
||||
trial_started_at: billing_customer
|
||||
.and_then(|billing_customer| billing_customer.trial_started_at)
|
||||
.map(|trial_started_at| {
|
||||
trial_started_at
|
||||
.and_utc()
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
}),
|
||||
max_monthly_llm_usage_spending_in_cents: preferences
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_MAX_MONTHLY_SPEND.0 as i32, |preferences| {
|
||||
@@ -136,8 +127,6 @@ async fn update_billing_preferences(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?;
|
||||
|
||||
let max_monthly_llm_usage_spending_in_cents =
|
||||
body.max_monthly_llm_usage_spending_in_cents.max(0);
|
||||
let model_request_overages_spend_limit_in_cents =
|
||||
@@ -193,13 +182,6 @@ async fn update_billing_preferences(
|
||||
rpc_server.refresh_llm_tokens_for_user(user.id).await;
|
||||
|
||||
Ok(Json(BillingPreferencesResponse {
|
||||
trial_started_at: billing_customer
|
||||
.and_then(|billing_customer| billing_customer.trial_started_at)
|
||||
.map(|trial_started_at| {
|
||||
trial_started_at
|
||||
.and_utc()
|
||||
.to_rfc3339_opts(SecondsFormat::Millis, true)
|
||||
}),
|
||||
max_monthly_llm_usage_spending_in_cents: billing_preferences
|
||||
.max_monthly_llm_usage_spending_in_cents,
|
||||
model_request_overages_enabled: billing_preferences.model_request_overages_enabled,
|
||||
@@ -319,6 +301,13 @@ async fn create_billing_subscription(
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
log::error!("failed to retrieve LLM database");
|
||||
Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
if app.db.has_active_billing_subscription(user.id).await? {
|
||||
return Err(Error::http(
|
||||
@@ -410,10 +399,16 @@ async fn create_billing_subscription(
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
return Err(Error::http(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"No product selected".into(),
|
||||
));
|
||||
let default_model = llm_db.model(
|
||||
zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
"claude-3-7-sonnet",
|
||||
)?;
|
||||
let stripe_model = stripe_billing
|
||||
.register_model_for_token_based_usage(default_model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1386,6 +1381,81 @@ async fn find_or_create_billing_customer(
|
||||
Ok(Some(billing_customer))
|
||||
}
|
||||
|
||||
const SYNC_LLM_TOKEN_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn sync_llm_token_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
let Some(stripe_billing) = app.stripe_billing.clone() else {
|
||||
log::warn!("failed to retrieve Stripe billing object");
|
||||
return;
|
||||
};
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
log::warn!("failed to retrieve LLM database");
|
||||
return;
|
||||
};
|
||||
|
||||
let executor = app.executor.clone();
|
||||
executor.spawn_detached({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
loop {
|
||||
sync_token_usage_with_stripe(&app, &llm_db, &stripe_billing)
|
||||
.await
|
||||
.context("failed to sync LLM usage to Stripe")
|
||||
.trace_err();
|
||||
executor
|
||||
.sleep(SYNC_LLM_TOKEN_USAGE_WITH_STRIPE_INTERVAL)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn sync_token_usage_with_stripe(
|
||||
app: &Arc<AppState>,
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
let events = llm_db.get_billing_events().await?;
|
||||
let user_ids = events
|
||||
.iter()
|
||||
.map(|(event, _)| event.user_id)
|
||||
.collect::<HashSet<UserId>>();
|
||||
let stripe_subscriptions = app.db.get_active_billing_subscriptions(user_ids).await?;
|
||||
|
||||
for (event, model) in events {
|
||||
let Some((stripe_db_customer, stripe_db_subscription)) =
|
||||
stripe_subscriptions.get(&event.user_id)
|
||||
else {
|
||||
tracing::warn!(
|
||||
user_id = event.user_id.0,
|
||||
"Registered billing event for user who is not a Stripe customer. Billing events should only be created for users who are Stripe customers, so this is a mistake on our side."
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let stripe_subscription_id: stripe::SubscriptionId = stripe_db_subscription
|
||||
.stripe_subscription_id
|
||||
.parse()
|
||||
.context("failed to parse stripe subscription id from db")?;
|
||||
let stripe_customer_id: stripe::CustomerId = stripe_db_customer
|
||||
.stripe_customer_id
|
||||
.parse()
|
||||
.context("failed to parse stripe customer id from db")?;
|
||||
|
||||
let stripe_model = stripe_billing
|
||||
.register_model_for_token_based_usage(&model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.subscribe_to_model(&stripe_subscription_id, &stripe_model)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.bill_model_token_usage(&stripe_customer_id, &stripe_model, &event)
|
||||
.await?;
|
||||
llm_db.consume_billing_event(event.id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SYNC_LLM_REQUEST_USAGE_WITH_STRIPE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
pub fn sync_llm_request_usage_with_stripe_periodically(app: Arc<AppState>) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
pub mod billing_events;
|
||||
pub mod providers;
|
||||
pub mod subscription_usage_meters;
|
||||
pub mod subscription_usages;
|
||||
|
||||
31
crates/collab/src/llm/db/queries/billing_events.rs
Normal file
31
crates/collab/src/llm/db/queries/billing_events.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use super::*;
|
||||
use crate::Result;
|
||||
use anyhow::Context as _;
|
||||
|
||||
impl LlmDatabase {
|
||||
pub async fn get_billing_events(&self) -> Result<Vec<(billing_event::Model, model::Model)>> {
|
||||
self.transaction(|tx| async move {
|
||||
let events_with_models = billing_event::Entity::find()
|
||||
.find_also_related(model::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
events_with_models
|
||||
.into_iter()
|
||||
.map(|(event, model)| {
|
||||
let model =
|
||||
model.context("could not find model associated with billing event")?;
|
||||
Ok((event, model))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn consume_billing_event(&self, id: BillingEventId) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
billing_event::Entity::delete_by_id(id).exec(&*tx).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod billing_event;
|
||||
pub mod model;
|
||||
pub mod monthly_usage;
|
||||
pub mod provider;
|
||||
|
||||
37
crates/collab/src/llm/db/tables/billing_event.rs
Normal file
37
crates/collab/src/llm/db/tables/billing_event.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use crate::{
|
||||
db::UserId,
|
||||
llm::db::{BillingEventId, ModelId},
|
||||
};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "billing_events")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: BillingEventId,
|
||||
pub idempotency_key: Uuid,
|
||||
pub user_id: UserId,
|
||||
pub model_id: ModelId,
|
||||
pub input_tokens: i64,
|
||||
pub input_cache_creation_tokens: i64,
|
||||
pub input_cache_read_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::model::Entity",
|
||||
from = "Column::ModelId",
|
||||
to = "super::model::Column::Id"
|
||||
)]
|
||||
Model,
|
||||
}
|
||||
|
||||
impl Related<super::model::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Model.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -31,6 +31,8 @@ pub enum Relation {
|
||||
Provider,
|
||||
#[sea_orm(has_many = "super::usage::Entity")]
|
||||
Usages,
|
||||
#[sea_orm(has_many = "super::billing_event::Entity")]
|
||||
BillingEvents,
|
||||
}
|
||||
|
||||
impl Related<super::provider::Entity> for Entity {
|
||||
@@ -45,4 +47,10 @@ impl Related<super::usage::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::billing_event::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::BillingEvents.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -8,7 +8,9 @@ use axum::{
|
||||
};
|
||||
|
||||
use collab::api::CloudflareIpCountryHeader;
|
||||
use collab::api::billing::sync_llm_request_usage_with_stripe_periodically;
|
||||
use collab::api::billing::{
|
||||
sync_llm_request_usage_with_stripe_periodically, sync_llm_token_usage_with_stripe_periodically,
|
||||
};
|
||||
use collab::llm::db::LlmDatabase;
|
||||
use collab::migrations::run_database_migrations;
|
||||
use collab::user_backfiller::spawn_user_backfiller;
|
||||
@@ -153,6 +155,7 @@ async fn main() -> Result<()> {
|
||||
if let Some(mut llm_db) = llm_db {
|
||||
llm_db.initialize().await?;
|
||||
sync_llm_request_usage_with_stripe_periodically(state.clone());
|
||||
sync_llm_token_usage_with_stripe_periodically(state.clone());
|
||||
}
|
||||
|
||||
app = app
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Result;
|
||||
use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
|
||||
use crate::llm::{self, AGENT_EXTENDED_TRIAL_FEATURE_FLAG};
|
||||
use crate::{Cents, Result};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use chrono::Utc;
|
||||
use chrono::{Datelike, Utc};
|
||||
use collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use stripe::PriceId;
|
||||
@@ -22,6 +22,18 @@ struct StripeBillingState {
|
||||
prices_by_lookup_key: HashMap<String, stripe::Price>,
|
||||
}
|
||||
|
||||
pub struct StripeModelTokenPrices {
|
||||
input_tokens_price: StripeBillingPrice,
|
||||
input_cache_creation_tokens_price: StripeBillingPrice,
|
||||
input_cache_read_tokens_price: StripeBillingPrice,
|
||||
output_tokens_price: StripeBillingPrice,
|
||||
}
|
||||
|
||||
struct StripeBillingPrice {
|
||||
id: stripe::PriceId,
|
||||
meter_event_name: String,
|
||||
}
|
||||
|
||||
impl StripeBilling {
|
||||
pub fn new(client: Arc<stripe::Client>) -> Self {
|
||||
Self {
|
||||
@@ -97,6 +109,142 @@ impl StripeBilling {
|
||||
.ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
|
||||
}
|
||||
|
||||
pub async fn register_model_for_token_based_usage(
|
||||
&self,
|
||||
model: &llm::db::model::Model,
|
||||
) -> Result<StripeModelTokenPrices> {
|
||||
let input_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/input_tokens", model.id),
|
||||
&format!("{} (Input Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let input_cache_creation_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/input_cache_creation_tokens", model.id),
|
||||
&format!("{} (Input Cache Creation Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_cache_creation_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let input_cache_read_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/input_cache_read_tokens", model.id),
|
||||
&format!("{} (Input Cache Read Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_cache_read_input_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
let output_tokens_price = self
|
||||
.get_or_insert_token_price(
|
||||
&format!("model_{}/output_tokens", model.id),
|
||||
&format!("{} (Output Tokens)", model.name),
|
||||
Cents::new(model.price_per_million_output_tokens as u32),
|
||||
)
|
||||
.await?;
|
||||
Ok(StripeModelTokenPrices {
|
||||
input_tokens_price,
|
||||
input_cache_creation_tokens_price,
|
||||
input_cache_read_tokens_price,
|
||||
output_tokens_price,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_or_insert_token_price(
|
||||
&self,
|
||||
meter_event_name: &str,
|
||||
price_description: &str,
|
||||
price_per_million_tokens: Cents,
|
||||
) -> Result<StripeBillingPrice> {
|
||||
// Fast code path when the meter and the price already exist.
|
||||
{
|
||||
let state = self.state.read().await;
|
||||
if let Some(meter) = state.meters_by_event_name.get(meter_event_name) {
|
||||
if let Some(price_id) = state.price_ids_by_meter_id.get(&meter.id) {
|
||||
return Ok(StripeBillingPrice {
|
||||
id: price_id.clone(),
|
||||
meter_event_name: meter_event_name.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = self.state.write().await;
|
||||
let meter = if let Some(meter) = state.meters_by_event_name.get(meter_event_name) {
|
||||
meter.clone()
|
||||
} else {
|
||||
let meter = StripeMeter::create(
|
||||
&self.client,
|
||||
StripeCreateMeterParams {
|
||||
default_aggregation: DefaultAggregation { formula: "sum" },
|
||||
display_name: price_description.to_string(),
|
||||
event_name: meter_event_name,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.meters_by_event_name
|
||||
.insert(meter_event_name.to_string(), meter.clone());
|
||||
meter
|
||||
};
|
||||
|
||||
let price_id = if let Some(price_id) = state.price_ids_by_meter_id.get(&meter.id) {
|
||||
price_id.clone()
|
||||
} else {
|
||||
let price = stripe::Price::create(
|
||||
&self.client,
|
||||
stripe::CreatePrice {
|
||||
active: Some(true),
|
||||
billing_scheme: Some(stripe::PriceBillingScheme::PerUnit),
|
||||
currency: stripe::Currency::USD,
|
||||
currency_options: None,
|
||||
custom_unit_amount: None,
|
||||
expand: &[],
|
||||
lookup_key: None,
|
||||
metadata: None,
|
||||
nickname: None,
|
||||
product: None,
|
||||
product_data: Some(stripe::CreatePriceProductData {
|
||||
id: None,
|
||||
active: Some(true),
|
||||
metadata: None,
|
||||
name: price_description.to_string(),
|
||||
statement_descriptor: None,
|
||||
tax_code: None,
|
||||
unit_label: None,
|
||||
}),
|
||||
recurring: Some(stripe::CreatePriceRecurring {
|
||||
aggregate_usage: None,
|
||||
interval: stripe::CreatePriceRecurringInterval::Month,
|
||||
interval_count: None,
|
||||
trial_period_days: None,
|
||||
usage_type: Some(stripe::CreatePriceRecurringUsageType::Metered),
|
||||
meter: Some(meter.id.clone()),
|
||||
}),
|
||||
tax_behavior: None,
|
||||
tiers: None,
|
||||
tiers_mode: None,
|
||||
transfer_lookup_key: None,
|
||||
transform_quantity: None,
|
||||
unit_amount: None,
|
||||
unit_amount_decimal: Some(&format!(
|
||||
"{:.12}",
|
||||
price_per_million_tokens.0 as f64 / 1_000_000f64
|
||||
)),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.price_ids_by_meter_id
|
||||
.insert(meter.id, price.id.clone());
|
||||
price.id
|
||||
};
|
||||
|
||||
Ok(StripeBillingPrice {
|
||||
id: price_id,
|
||||
meter_event_name: meter_event_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_price(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
@@ -135,6 +283,142 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_to_model(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
model: &StripeModelTokenPrices,
|
||||
) -> Result<()> {
|
||||
let subscription =
|
||||
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
if !subscription_contains_price(&subscription, &model.input_tokens_price.id) {
|
||||
items.push(stripe::UpdateSubscriptionItems {
|
||||
price: Some(model.input_tokens_price.id.to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if !subscription_contains_price(&subscription, &model.input_cache_creation_tokens_price.id)
|
||||
{
|
||||
items.push(stripe::UpdateSubscriptionItems {
|
||||
price: Some(model.input_cache_creation_tokens_price.id.to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if !subscription_contains_price(&subscription, &model.input_cache_read_tokens_price.id) {
|
||||
items.push(stripe::UpdateSubscriptionItems {
|
||||
price: Some(model.input_cache_read_tokens_price.id.to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if !subscription_contains_price(&subscription, &model.output_tokens_price.id) {
|
||||
items.push(stripe::UpdateSubscriptionItems {
|
||||
price: Some(model.output_tokens_price.id.to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if !items.is_empty() {
|
||||
items.extend(subscription.items.data.iter().map(|item| {
|
||||
stripe::UpdateSubscriptionItems {
|
||||
id: Some(item.id.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}));
|
||||
|
||||
stripe::Subscription::update(
|
||||
&self.client,
|
||||
subscription_id,
|
||||
stripe::UpdateSubscription {
|
||||
items: Some(items),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bill_model_token_usage(
|
||||
&self,
|
||||
customer_id: &stripe::CustomerId,
|
||||
model: &StripeModelTokenPrices,
|
||||
event: &llm::db::billing_event::Model,
|
||||
) -> Result<()> {
|
||||
let timestamp = Utc::now().timestamp();
|
||||
|
||||
if event.input_tokens > 0 {
|
||||
StripeMeterEvent::create(
|
||||
&self.client,
|
||||
StripeCreateMeterEventParams {
|
||||
identifier: &format!("input_tokens/{}", event.idempotency_key),
|
||||
event_name: &model.input_tokens_price.meter_event_name,
|
||||
payload: StripeCreateMeterEventPayload {
|
||||
value: event.input_tokens as u64,
|
||||
stripe_customer_id: customer_id,
|
||||
},
|
||||
timestamp: Some(timestamp),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if event.input_cache_creation_tokens > 0 {
|
||||
StripeMeterEvent::create(
|
||||
&self.client,
|
||||
StripeCreateMeterEventParams {
|
||||
identifier: &format!("input_cache_creation_tokens/{}", event.idempotency_key),
|
||||
event_name: &model.input_cache_creation_tokens_price.meter_event_name,
|
||||
payload: StripeCreateMeterEventPayload {
|
||||
value: event.input_cache_creation_tokens as u64,
|
||||
stripe_customer_id: customer_id,
|
||||
},
|
||||
timestamp: Some(timestamp),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if event.input_cache_read_tokens > 0 {
|
||||
StripeMeterEvent::create(
|
||||
&self.client,
|
||||
StripeCreateMeterEventParams {
|
||||
identifier: &format!("input_cache_read_tokens/{}", event.idempotency_key),
|
||||
event_name: &model.input_cache_read_tokens_price.meter_event_name,
|
||||
payload: StripeCreateMeterEventPayload {
|
||||
value: event.input_cache_read_tokens as u64,
|
||||
stripe_customer_id: customer_id,
|
||||
},
|
||||
timestamp: Some(timestamp),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if event.output_tokens > 0 {
|
||||
StripeMeterEvent::create(
|
||||
&self.client,
|
||||
StripeCreateMeterEventParams {
|
||||
identifier: &format!("output_tokens/{}", event.idempotency_key),
|
||||
event_name: &model.output_tokens_price.meter_event_name,
|
||||
payload: StripeCreateMeterEventPayload {
|
||||
value: event.output_tokens as u64,
|
||||
stripe_customer_id: customer_id,
|
||||
},
|
||||
timestamp: Some(timestamp),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn bill_model_request_usage(
|
||||
&self,
|
||||
customer_id: &stripe::CustomerId,
|
||||
@@ -161,6 +445,47 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn checkout(
|
||||
&self,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
model: &StripeModelTokenPrices,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let first_of_next_month = Utc::now()
|
||||
.checked_add_months(chrono::Months::new(1))
|
||||
.unwrap()
|
||||
.with_day(1)
|
||||
.unwrap();
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.subscription_data = Some(stripe::CreateCheckoutSessionSubscriptionData {
|
||||
billing_cycle_anchor: Some(first_of_next_month.timestamp()),
|
||||
..Default::default()
|
||||
});
|
||||
params.line_items = Some(
|
||||
[
|
||||
&model.input_tokens_price.id,
|
||||
&model.input_cache_creation_tokens_price.id,
|
||||
&model.input_cache_read_tokens_price.id,
|
||||
&model.output_tokens_price.id,
|
||||
]
|
||||
.into_iter()
|
||||
.map(|price_id| stripe::CreateCheckoutSessionLineItems {
|
||||
price: Some(price_id.to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
params.success_url = Some(success_url);
|
||||
|
||||
let session = stripe::CheckoutSession::create(&self.client, params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
|
||||
pub async fn checkout_with_zed_pro(
|
||||
&self,
|
||||
customer_id: stripe::CustomerId,
|
||||
@@ -262,6 +587,18 @@ impl StripeBilling {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DefaultAggregation {
|
||||
formula: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StripeCreateMeterParams<'a> {
|
||||
default_aggregation: DefaultAggregation,
|
||||
display_name: String,
|
||||
event_name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
struct StripeMeter {
|
||||
id: String,
|
||||
@@ -269,6 +606,13 @@ struct StripeMeter {
|
||||
}
|
||||
|
||||
impl StripeMeter {
|
||||
pub fn create(
|
||||
client: &stripe::Client,
|
||||
params: StripeCreateMeterParams,
|
||||
) -> stripe::Response<Self> {
|
||||
client.post_form("/billing/meters", params)
|
||||
}
|
||||
|
||||
pub fn list(client: &stripe::Client) -> stripe::Response<stripe::List<Self>> {
|
||||
#[derive(Serialize)]
|
||||
struct Params {
|
||||
|
||||
@@ -319,7 +319,7 @@ impl ChatPanel {
|
||||
None => {
|
||||
return div().child(
|
||||
h_flex()
|
||||
.text_ui_xs()
|
||||
.text_ui_xs(cx)
|
||||
.my_0p5()
|
||||
.px_0p5()
|
||||
.gap_x_1()
|
||||
@@ -354,7 +354,7 @@ impl ChatPanel {
|
||||
div().child(
|
||||
h_flex()
|
||||
.id(message_element_id)
|
||||
.text_ui_xs()
|
||||
.text_ui_xs(cx)
|
||||
.my_0p5()
|
||||
.px_0p5()
|
||||
.gap_x_1()
|
||||
@@ -504,7 +504,7 @@ impl ChatPanel {
|
||||
this.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.child(
|
||||
Avatar::new(message.sender.avatar_uri.clone())
|
||||
.size(rems(1.)),
|
||||
@@ -541,7 +541,7 @@ impl ChatPanel {
|
||||
el.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.id(element_id)
|
||||
.child(text.element("body".into(), window, cx)),
|
||||
)
|
||||
@@ -564,7 +564,7 @@ impl ChatPanel {
|
||||
div()
|
||||
.px_1()
|
||||
.rounded_sm()
|
||||
.text_ui_xs()
|
||||
.text_ui_xs(cx)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child("New messages"),
|
||||
)
|
||||
@@ -1011,7 +1011,7 @@ impl Render for ChatPanel {
|
||||
el.child(
|
||||
h_flex()
|
||||
.px_2()
|
||||
.text_ui_xs()
|
||||
.text_ui_xs(cx)
|
||||
.justify_between()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
|
||||
@@ -524,7 +524,7 @@ impl Render for MessageEditor {
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: TextSize::Small.rems().into(),
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
font_style: FontStyle::Normal,
|
||||
line_height: relative(1.3),
|
||||
|
||||
@@ -22,7 +22,6 @@ use ui::{
|
||||
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::SuppressNotification;
|
||||
use workspace::notifications::{
|
||||
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
|
||||
};
|
||||
@@ -824,19 +823,11 @@ impl Render for NotificationToast {
|
||||
.child(Label::new(self.text.clone()))
|
||||
.child(
|
||||
IconButton::new("close", IconName::Close)
|
||||
.tooltip(|window, cx| Tooltip::for_action("Close", &menu::Cancel, window, cx))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("suppress", IconName::SquareMinus)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Do not show until restart",
|
||||
&SuppressNotification,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
IconButton::new("suppress", IconName::XCircle)
|
||||
.tooltip(Tooltip::text("Do not show until restart"))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
|
||||
@@ -34,7 +34,7 @@ impl ParentElement for CollabNotification {
|
||||
impl RenderOnce for CollabNotification {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.text_ui()
|
||||
.text_ui(cx)
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
|
||||
@@ -629,7 +629,7 @@ impl ComponentPreview {
|
||||
.when_some(description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.text_ui_sm()
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.max_w(px(600.0))
|
||||
.child(description),
|
||||
|
||||
@@ -36,6 +36,7 @@ gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp-types.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
|
||||
@@ -402,6 +402,10 @@ pub async fn fetch_latest_adapter_version_from_github(
|
||||
})
|
||||
}
|
||||
|
||||
pub trait InlineValueProvider {
|
||||
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue>;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait DebugAdapter: 'static + Send + Sync {
|
||||
fn name(&self) -> DebugAdapterName;
|
||||
@@ -413,6 +417,10 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary>;
|
||||
|
||||
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod adapters;
|
||||
pub mod client;
|
||||
pub mod debugger_settings;
|
||||
pub mod inline_value;
|
||||
pub mod proto_conversions;
|
||||
mod registry;
|
||||
pub mod transport;
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum VariableLookupKind {
|
||||
Variable,
|
||||
Expression,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum VariableScope {
|
||||
Local,
|
||||
Global,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InlineValueLocation {
|
||||
pub variable_name: String,
|
||||
pub scope: VariableScope,
|
||||
pub lookup: VariableLookupKind,
|
||||
pub row: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
/// A trait for providing inline values for debugging purposes.
|
||||
///
|
||||
/// Implementors of this trait are responsible for analyzing a given node in the
|
||||
/// source code and extracting variable information, including their names,
|
||||
/// scopes, and positions. This information is used to display inline values
|
||||
/// during debugging sessions. Implementors must also handle variable scoping
|
||||
/// themselves by traversing the syntax tree upwards to determine whether a
|
||||
/// variable is local or global.
|
||||
pub trait InlineValueProvider {
|
||||
/// Provides a list of inline value locations based on the given node and source code.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `node`: The root node of the active debug line. Implementors should traverse
|
||||
/// upwards from this node to gather variable information and determine their scope.
|
||||
/// - `source`: The source code as a string slice, used to extract variable names.
|
||||
/// - `max_row`: The maximum row to consider when collecting variables. Variables
|
||||
/// declared beyond this row should be ignored.
|
||||
///
|
||||
/// # Returns
|
||||
/// A vector of `InlineValueLocation` instances, each representing a variable's
|
||||
/// name, scope, and the position of the inline value should be shown.
|
||||
fn provide(
|
||||
&self,
|
||||
node: language::Node,
|
||||
source: &str,
|
||||
max_row: usize,
|
||||
) -> Vec<InlineValueLocation>;
|
||||
}
|
||||
|
||||
pub struct RustInlineValueProvider;
|
||||
|
||||
impl InlineValueProvider for RustInlineValueProvider {
|
||||
fn provide(
|
||||
&self,
|
||||
mut node: language::Node,
|
||||
source: &str,
|
||||
max_row: usize,
|
||||
) -> Vec<InlineValueLocation> {
|
||||
let mut variables = Vec::new();
|
||||
let mut variable_names = HashSet::new();
|
||||
let mut scope = VariableScope::Local;
|
||||
|
||||
loop {
|
||||
let mut variable_names_in_scope = HashMap::new();
|
||||
for child in node.named_children(&mut node.walk()) {
|
||||
if child.start_position().row >= max_row {
|
||||
break;
|
||||
}
|
||||
|
||||
if scope == VariableScope::Local && child.kind() == "let_declaration" {
|
||||
if let Some(identifier) = child.child_by_field_name("pattern") {
|
||||
let variable_name = source[identifier.byte_range()].to_string();
|
||||
|
||||
if variable_names.contains(&variable_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) = variable_names_in_scope.get(&variable_name) {
|
||||
variables.remove(*index);
|
||||
}
|
||||
|
||||
variable_names_in_scope.insert(variable_name.clone(), variables.len());
|
||||
variables.push(InlineValueLocation {
|
||||
variable_name,
|
||||
scope: VariableScope::Local,
|
||||
lookup: VariableLookupKind::Variable,
|
||||
row: identifier.end_position().row,
|
||||
column: identifier.end_position().column,
|
||||
});
|
||||
}
|
||||
} else if child.kind() == "static_item" {
|
||||
if let Some(name) = child.child_by_field_name("name") {
|
||||
let variable_name = source[name.byte_range()].to_string();
|
||||
variables.push(InlineValueLocation {
|
||||
variable_name,
|
||||
scope: scope.clone(),
|
||||
lookup: VariableLookupKind::Expression,
|
||||
row: name.end_position().row,
|
||||
column: name.end_position().column,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable_names.extend(variable_names_in_scope.keys().cloned());
|
||||
|
||||
if matches!(node.kind(), "function_item" | "closure_expression") {
|
||||
scope = VariableScope::Global;
|
||||
}
|
||||
|
||||
if let Some(parent) = node.parent() {
|
||||
node = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
variables
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PythonInlineValueProvider;
|
||||
|
||||
impl InlineValueProvider for PythonInlineValueProvider {
|
||||
fn provide(
|
||||
&self,
|
||||
mut node: language::Node,
|
||||
source: &str,
|
||||
max_row: usize,
|
||||
) -> Vec<InlineValueLocation> {
|
||||
let mut variables = Vec::new();
|
||||
let mut variable_names = HashSet::new();
|
||||
let mut scope = VariableScope::Local;
|
||||
|
||||
loop {
|
||||
let mut variable_names_in_scope = HashMap::new();
|
||||
for child in node.named_children(&mut node.walk()) {
|
||||
if child.start_position().row >= max_row {
|
||||
break;
|
||||
}
|
||||
|
||||
if scope == VariableScope::Local {
|
||||
match child.kind() {
|
||||
"expression_statement" => {
|
||||
if let Some(expr) = child.child(0) {
|
||||
if expr.kind() == "assignment" {
|
||||
if let Some(param) = expr.child(0) {
|
||||
let param_identifier = if param.kind() == "identifier" {
|
||||
Some(param)
|
||||
} else if param.kind() == "typed_parameter" {
|
||||
param.child(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(identifier) = param_identifier {
|
||||
if identifier.kind() == "identifier" {
|
||||
let variable_name =
|
||||
source[identifier.byte_range()].to_string();
|
||||
|
||||
if variable_names.contains(&variable_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) =
|
||||
variable_names_in_scope.get(&variable_name)
|
||||
{
|
||||
variables.remove(*index);
|
||||
}
|
||||
|
||||
variable_names_in_scope
|
||||
.insert(variable_name.clone(), variables.len());
|
||||
variables.push(InlineValueLocation {
|
||||
variable_name,
|
||||
scope: VariableScope::Local,
|
||||
lookup: VariableLookupKind::Variable,
|
||||
row: identifier.end_position().row,
|
||||
column: identifier.end_position().column,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"function_definition" => {
|
||||
if let Some(params) = child.child_by_field_name("parameters") {
|
||||
for param in params.named_children(&mut params.walk()) {
|
||||
let param_identifier = if param.kind() == "identifier" {
|
||||
Some(param)
|
||||
} else if param.kind() == "typed_parameter" {
|
||||
param.child(0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(identifier) = param_identifier {
|
||||
if identifier.kind() == "identifier" {
|
||||
let variable_name =
|
||||
source[identifier.byte_range()].to_string();
|
||||
|
||||
if variable_names.contains(&variable_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) =
|
||||
variable_names_in_scope.get(&variable_name)
|
||||
{
|
||||
variables.remove(*index);
|
||||
}
|
||||
|
||||
variable_names_in_scope
|
||||
.insert(variable_name.clone(), variables.len());
|
||||
variables.push(InlineValueLocation {
|
||||
variable_name,
|
||||
scope: VariableScope::Local,
|
||||
lookup: VariableLookupKind::Variable,
|
||||
row: identifier.end_position().row,
|
||||
column: identifier.end_position().column,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"for_statement" => {
|
||||
if let Some(target) = child.child_by_field_name("left") {
|
||||
if target.kind() == "identifier" {
|
||||
let variable_name = source[target.byte_range()].to_string();
|
||||
|
||||
if variable_names.contains(&variable_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(index) = variable_names_in_scope.get(&variable_name)
|
||||
{
|
||||
variables.remove(*index);
|
||||
}
|
||||
|
||||
variable_names_in_scope
|
||||
.insert(variable_name.clone(), variables.len());
|
||||
variables.push(InlineValueLocation {
|
||||
variable_name,
|
||||
scope: VariableScope::Local,
|
||||
lookup: VariableLookupKind::Variable,
|
||||
row: target.end_position().row,
|
||||
column: target.end_position().column,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable_names.extend(variable_names_in_scope.keys().cloned());
|
||||
|
||||
if matches!(node.kind(), "function_definition" | "module")
|
||||
&& node.range().end_point.row < max_row
|
||||
{
|
||||
scope = VariableScope::Global;
|
||||
}
|
||||
|
||||
if let Some(parent) = node.parent() {
|
||||
node = parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
variables
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,7 @@ use gpui::{App, Global, SharedString};
|
||||
use parking_lot::RwLock;
|
||||
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
|
||||
use crate::{
|
||||
adapters::{DebugAdapter, DebugAdapterName},
|
||||
inline_value::InlineValueProvider,
|
||||
};
|
||||
use crate::adapters::{DebugAdapter, DebugAdapterName};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
|
||||
@@ -25,7 +22,6 @@ pub trait DapLocator: Send + Sync {
|
||||
struct DapRegistryState {
|
||||
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
|
||||
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
|
||||
inline_value_providers: FxHashMap<String, Arc<dyn InlineValueProvider>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -62,22 +58,6 @@ impl DapRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_inline_value_provider(
|
||||
&self,
|
||||
language: String,
|
||||
provider: Arc<dyn InlineValueProvider>,
|
||||
) {
|
||||
let _previous_value = self
|
||||
.0
|
||||
.write()
|
||||
.inline_value_providers
|
||||
.insert(language, provider);
|
||||
debug_assert!(
|
||||
_previous_value.is_none(),
|
||||
"Attempted to insert a new inline value provider when one is already registered"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
|
||||
self.0.read().locators.clone()
|
||||
}
|
||||
@@ -86,10 +66,6 @@ impl DapRegistry {
|
||||
self.0.read().adapters.get(name).cloned()
|
||||
}
|
||||
|
||||
pub fn inline_value_provider(&self, language: &str) -> Option<Arc<dyn InlineValueProvider>> {
|
||||
self.0.read().inline_value_providers.get(language).cloned()
|
||||
}
|
||||
|
||||
pub fn enumerate_adapters(&self) -> Vec<DebugAdapterName> {
|
||||
self.0.read().adapters.keys().cloned().collect()
|
||||
}
|
||||
|
||||
@@ -580,31 +580,21 @@ impl TcpTransport {
|
||||
.unwrap_or(2000u64)
|
||||
});
|
||||
|
||||
let (mut process, (rx, tx)) = select! {
|
||||
let (rx, tx) = select! {
|
||||
_ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => {
|
||||
return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", host, port)))
|
||||
},
|
||||
result = cx.spawn(async move |cx| {
|
||||
loop {
|
||||
match TcpStream::connect(address).await {
|
||||
Ok(stream) => return Ok((process, stream.split())),
|
||||
Ok(stream) => return stream.split(),
|
||||
Err(_) => {
|
||||
if let Ok(Some(_)) = process.try_status() {
|
||||
let output = process.output().await?;
|
||||
let output = if output.stderr.is_empty() {
|
||||
String::from_utf8_lossy(&output.stdout).to_string()
|
||||
} else {
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
};
|
||||
return Err(anyhow!("{}\nerror: process exited before debugger attached.", output));
|
||||
}
|
||||
cx.background_executor().timer(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).fuse() => result?
|
||||
}).fuse() => result
|
||||
};
|
||||
|
||||
log::info!(
|
||||
"Debug adapter has connected to TCP server {}:{}",
|
||||
host,
|
||||
|
||||
@@ -27,6 +27,7 @@ dap.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
lsp-types.workspace = true
|
||||
paths.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dap::adapters::{DebugTaskDefinition, latest_github_release};
|
||||
use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
|
||||
use futures::StreamExt;
|
||||
use gpui::AsyncApp;
|
||||
use task::DebugRequest;
|
||||
@@ -159,4 +159,25 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
|
||||
Some(Box::new(CodeLldbInlineValueProvider))
|
||||
}
|
||||
}
|
||||
|
||||
struct CodeLldbInlineValueProvider;
|
||||
|
||||
impl InlineValueProvider for CodeLldbInlineValueProvider {
|
||||
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
|
||||
variables
|
||||
.into_iter()
|
||||
.map(|(variable, range)| {
|
||||
lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup {
|
||||
range,
|
||||
variable_name: Some(variable),
|
||||
case_sensitive_lookup: true,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ mod go;
|
||||
mod javascript;
|
||||
mod php;
|
||||
mod python;
|
||||
mod ruby;
|
||||
|
||||
use std::{net::Ipv4Addr, sync::Arc};
|
||||
|
||||
@@ -17,7 +16,6 @@ use dap::{
|
||||
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
|
||||
GithubRepo,
|
||||
},
|
||||
inline_value::{PythonInlineValueProvider, RustInlineValueProvider},
|
||||
};
|
||||
use gdb::GdbDebugAdapter;
|
||||
use go::GoDebugAdapter;
|
||||
@@ -25,7 +23,6 @@ use gpui::{App, BorrowAppContext};
|
||||
use javascript::JsDebugAdapter;
|
||||
use php::PhpDebugAdapter;
|
||||
use python::PythonDebugAdapter;
|
||||
use ruby::RubyDebugAdapter;
|
||||
use serde_json::{Value, json};
|
||||
use task::TcpArgumentsTemplate;
|
||||
|
||||
@@ -35,13 +32,8 @@ pub fn init(cx: &mut App) {
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(RubyDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
|
||||
registry.add_inline_value_provider("Rust".to_string(), Arc::from(RustInlineValueProvider));
|
||||
registry
|
||||
.add_inline_value_provider("Python".to_string(), Arc::from(PythonInlineValueProvider));
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::*;
|
||||
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition,
|
||||
adapters::InlineValueProvider,
|
||||
};
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use util::ResultExt;
|
||||
@@ -179,4 +182,34 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
|
||||
Some(Box::new(PythonInlineValueProvider))
|
||||
}
|
||||
}
|
||||
|
||||
struct PythonInlineValueProvider;
|
||||
|
||||
impl InlineValueProvider for PythonInlineValueProvider {
|
||||
fn provide(&self, variables: Vec<(String, lsp_types::Range)>) -> Vec<lsp_types::InlineValue> {
|
||||
variables
|
||||
.into_iter()
|
||||
.map(|(variable, range)| {
|
||||
if variable.contains(".") || variable.contains("[") {
|
||||
lsp_types::InlineValue::EvaluatableExpression(
|
||||
lsp_types::InlineValueEvaluatableExpression {
|
||||
range,
|
||||
expression: Some(variable),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
lsp_types::InlineValue::VariableLookup(lsp_types::InlineValueVariableLookup {
|
||||
range,
|
||||
variable_name: Some(variable),
|
||||
case_sensitive_lookup: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments,
|
||||
adapters::{
|
||||
self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
},
|
||||
};
|
||||
use gpui::AsyncApp;
|
||||
use std::path::PathBuf;
|
||||
use util::command::new_smol_command;
|
||||
|
||||
use crate::ToDap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RubyDebugAdapter;
|
||||
|
||||
impl RubyDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Ruby";
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for RubyDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
definition: &DebugTaskDefinition,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
|
||||
let mut rdbg_path = adapter_path.join("rdbg");
|
||||
if !delegate.fs().is_file(&rdbg_path).await {
|
||||
match delegate.which("rdbg".as_ref()) {
|
||||
Some(path) => rdbg_path = path,
|
||||
None => {
|
||||
delegate.output_to_console(
|
||||
"rdbg not found on path, trying `gem install debug`".to_string(),
|
||||
);
|
||||
let output = new_smol_command("gem")
|
||||
.arg("install")
|
||||
.arg("--no-document")
|
||||
.arg("--bindir")
|
||||
.arg(adapter_path)
|
||||
.arg("debug")
|
||||
.output()
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"Failed to install rdbg:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
|
||||
let DebugRequest::Launch(mut launch) = definition.request.clone() else {
|
||||
anyhow::bail!("rdbg does not yet support attaching");
|
||||
};
|
||||
|
||||
let mut arguments = vec![
|
||||
"--open".to_string(),
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
];
|
||||
if launch.args.is_empty() {
|
||||
let program = launch.program.clone();
|
||||
let mut split = program.split(" ");
|
||||
launch.program = split.next().unwrap().to_string();
|
||||
launch.args = split.map(|s| s.to_string()).collect();
|
||||
}
|
||||
if delegate.which(launch.program.as_ref()).is_some() {
|
||||
arguments.push("--command".to_string())
|
||||
}
|
||||
arguments.push(launch.program);
|
||||
arguments.extend(launch.args);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: rdbg_path.to_string_lossy().to_string(),
|
||||
arguments,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
cwd: launch.cwd,
|
||||
envs: launch.env.into_iter().collect(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration: serde_json::Value::Object(Default::default()),
|
||||
request: definition.request.to_dap(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ doctest = false
|
||||
[features]
|
||||
test-support = [
|
||||
"dap/test-support",
|
||||
"dap_adapters/test-support",
|
||||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
@@ -32,7 +31,6 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
dap.workspace = true
|
||||
dap_adapters = { workspace = true, optional = true }
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
@@ -65,7 +63,6 @@ unindent = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
dap_adapters = { workspace = true, features = ["test-support"] }
|
||||
debugger_tools = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user