Compare commits
95 Commits
editor-foc
...
agent-remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
854b9b85a3 | ||
|
|
84efe57c00 | ||
|
|
d34e501b38 | ||
|
|
58cf69704d | ||
|
|
59cbc05698 | ||
|
|
3173f87dc3 | ||
|
|
6592314984 | ||
|
|
93b6fdb8e5 | ||
|
|
e79d1b27b1 | ||
|
|
1a0eedb787 | ||
|
|
8db0333b04 | ||
|
|
a13c8b70dd | ||
|
|
ddc649bdb8 | ||
|
|
33c896c23d | ||
|
|
19b6c4444e | ||
|
|
8e39281699 | ||
|
|
8294981ab5 | ||
|
|
a3105c92a4 | ||
|
|
a6c3d49bb9 | ||
|
|
5a38bbbd22 | ||
|
|
196586e352 | ||
|
|
a1d8e50ec1 | ||
|
|
24bc9fd0a0 | ||
|
|
03f02804e5 | ||
|
|
41b0a5cf10 | ||
|
|
739236e968 | ||
|
|
f14e48d202 | ||
|
|
634b275931 | ||
|
|
8000151aa9 | ||
|
|
f0f0a52793 | ||
|
|
907b2f0521 | ||
|
|
0ad582eec4 | ||
|
|
58ed81b698 | ||
|
|
83319c8a6d | ||
|
|
4deb8cce8d | ||
|
|
8d79226445 | ||
|
|
5abca0f867 | ||
|
|
68945ac53e | ||
|
|
49887d6934 | ||
|
|
d867897746 | ||
|
|
1f58ce80f2 | ||
|
|
ed772e6baf | ||
|
|
559725d8f5 | ||
|
|
f0da3b74f8 | ||
|
|
cee9f4b013 | ||
|
|
ae31aa2759 | ||
|
|
82a7aca5a6 | ||
|
|
b34f19a46f | ||
|
|
09ace088ac | ||
|
|
49ba4ed49c | ||
|
|
06af0310f7 | ||
|
|
1fa19c69a6 | ||
|
|
5ba1d3edec | ||
|
|
e4525b80f8 | ||
|
|
18a2a50227 | ||
|
|
172a475515 | ||
|
|
471e02d48f | ||
|
|
39da72161f | ||
|
|
daa777440d | ||
|
|
79ba22673b | ||
|
|
074e78301a | ||
|
|
fbeee1f832 | ||
|
|
bff259731f | ||
|
|
6c5b9b43c1 | ||
|
|
f29c6e5661 | ||
|
|
000077facf | ||
|
|
2b249f9e68 | ||
|
|
e13ecc07bc | ||
|
|
bef25c7290 | ||
|
|
65b13968a2 | ||
|
|
9afc6f6f5c | ||
|
|
82d271cb5b | ||
|
|
77ad6d7fbb | ||
|
|
d6ab416168 | ||
|
|
8f07135201 | ||
|
|
1dfddf0a29 | ||
|
|
cf8f003916 | ||
|
|
00292450e0 | ||
|
|
49c01c60b7 | ||
|
|
863d7ccb6d | ||
|
|
d270f6b953 | ||
|
|
08f516ce9a | ||
|
|
9cff5cfe3a | ||
|
|
0abee5668a | ||
|
|
c58b6903b8 | ||
|
|
11b6ce46e2 | ||
|
|
8c8357387e | ||
|
|
25ced2e3c2 | ||
|
|
f248da5921 | ||
|
|
89ce49d5b7 | ||
|
|
30f3efe697 | ||
|
|
023a60806a | ||
|
|
2c602bb0e5 | ||
|
|
857134d6dc | ||
|
|
d8980c25d2 |
53
Cargo.lock
generated
53
Cargo.lock
generated
@@ -539,7 +539,6 @@ dependencies = [
|
||||
"indexmap",
|
||||
"language_model",
|
||||
"lmstudio",
|
||||
"log",
|
||||
"ollama",
|
||||
"open_ai",
|
||||
"paths",
|
||||
@@ -3068,6 +3067,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"notifications",
|
||||
"picker",
|
||||
@@ -3183,32 +3183,6 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent",
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"db",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"languages",
|
||||
"log",
|
||||
"notifications",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"serde",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@@ -3334,6 +3308,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"indoc",
|
||||
"inline_completion",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"lsp",
|
||||
@@ -3343,11 +3318,9 @@ dependencies = [
|
||||
"paths",
|
||||
"project",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"task",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -3533,9 +3506,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.13.2"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e418dd4f5128c3e93eab12246391c54a20c496811131f85754dc8152ee207892"
|
||||
checksum = "3e1ecbb5db9a4c2ee642df67bcfa8f044dd867dbbaa21bfab139cbc204ffbf67"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"fontdb 0.16.2",
|
||||
@@ -4208,6 +4181,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"shlex",
|
||||
"sysinfo",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
@@ -7250,7 +7224,6 @@ dependencies = [
|
||||
"lsp",
|
||||
"paths",
|
||||
"project",
|
||||
"proto",
|
||||
"regex",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -7258,7 +7231,6 @@ dependencies = [
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
@@ -7813,9 +7785,12 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language_model",
|
||||
"log",
|
||||
"ordered-float 2.10.1",
|
||||
"picker",
|
||||
"proto",
|
||||
"ui",
|
||||
@@ -7840,7 +7815,6 @@ dependencies = [
|
||||
"deepseek",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"google_ai",
|
||||
"gpui",
|
||||
@@ -18559,6 +18533,7 @@ dependencies = [
|
||||
"assets",
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"async-watch",
|
||||
"audio",
|
||||
@@ -18575,7 +18550,7 @@ dependencies = [
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"component_preview",
|
||||
"component",
|
||||
"copilot",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
@@ -18601,6 +18576,7 @@ dependencies = [
|
||||
"gpui_tokio",
|
||||
"http_client",
|
||||
"image_viewer",
|
||||
"indoc",
|
||||
"inline_completion_button",
|
||||
"install_cli",
|
||||
"journal",
|
||||
@@ -18613,6 +18589,7 @@ dependencies = [
|
||||
"languages",
|
||||
"libc",
|
||||
"log",
|
||||
"markdown",
|
||||
"markdown_preview",
|
||||
"menu",
|
||||
"migrator",
|
||||
@@ -18664,6 +18641,7 @@ dependencies = [
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"ui_prompt",
|
||||
"url",
|
||||
"urlencoding",
|
||||
@@ -18738,9 +18716,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23b2fd00776b0c55072f389654910ceb501eb0083d7f78905ab0e5cc86949ec"
|
||||
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -18948,6 +18926,7 @@ dependencies = [
|
||||
"paths",
|
||||
"postage",
|
||||
"project",
|
||||
"proto",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
|
||||
@@ -31,7 +31,6 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/command_palette_hooks",
|
||||
"crates/component",
|
||||
"crates/component_preview",
|
||||
"crates/context_server",
|
||||
"crates/copilot",
|
||||
"crates/credentials_provider",
|
||||
@@ -238,7 +237,6 @@ collections = { path = "crates/collections" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
component_preview = { path = "crates/component_preview" }
|
||||
context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
credentials_provider = { path = "crates/credentials_provider" }
|
||||
@@ -608,7 +606,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.8.1"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
1
assets/icons/load_circle.svg
Normal file
1
assets/icons/load_circle.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
||||
|
After Width: | Height: | Size: 289 B |
@@ -244,7 +244,7 @@
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus"
|
||||
}
|
||||
@@ -538,6 +538,7 @@
|
||||
"ctrl-alt-b": "workspace::ToggleRightDock",
|
||||
"ctrl-b": "workspace::ToggleLeftDock",
|
||||
"ctrl-j": "workspace::ToggleBottomDock",
|
||||
"ctrl-w": "workspace::CloseActiveDock",
|
||||
"ctrl-alt-y": "workspace::CloseAllDocks",
|
||||
"shift-find": "pane::DeploySearch",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
|
||||
@@ -290,7 +290,7 @@
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-shift-o": "agent::ToggleNavigationMenu",
|
||||
"cmd-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus"
|
||||
}
|
||||
@@ -608,6 +608,7 @@
|
||||
"cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-r": "workspace::ToggleRightDock",
|
||||
"cmd-j": "workspace::ToggleBottomDock",
|
||||
"cmd-w": "workspace::CloseActiveDock",
|
||||
"alt-cmd-y": "workspace::CloseAllDocks",
|
||||
"cmd-shift-f": "pane::DeploySearch",
|
||||
"cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
|
||||
@@ -49,10 +49,9 @@ And here's the section to rewrite based on that prompt again for reference:
|
||||
</rewrite_this>
|
||||
|
||||
{{#if diagnostic_errors}}
|
||||
{{#each diagnostic_errors}}
|
||||
|
||||
Below are the diagnostic errors visible to the user. If the user requests problems to be fixed, use this information, but do not try to fix these errors if the user hasn't asked you to.
|
||||
|
||||
{{#each diagnostic_errors}}
|
||||
<diagnostic_error>
|
||||
<line_number>{{line_number}}</line_number>
|
||||
<error_message>{{error_message}}</error_message>
|
||||
|
||||
@@ -328,6 +328,10 @@
|
||||
"title_bar": {
|
||||
// Whether to show the branch icon beside branch switcher in the titlebar.
|
||||
"show_branch_icon": false,
|
||||
// Whether to show the branch name button in the titlebar.
|
||||
"show_branch_name": true,
|
||||
// Whether to show the project host and name in the titlebar.
|
||||
"show_project_items": true,
|
||||
// Whether to show onboarding banners in the titlebar.
|
||||
"show_onboarding_banner": true,
|
||||
// Whether to show user picture in the titlebar.
|
||||
@@ -470,6 +474,8 @@
|
||||
"search_wrap": true,
|
||||
// Search options to enable by default when opening new project and buffer searches.
|
||||
"search": {
|
||||
// Whether to show the project search button in the status bar.
|
||||
"button": true,
|
||||
"whole_word": false,
|
||||
"case_sensitive": false,
|
||||
"include_ignored": false,
|
||||
@@ -694,8 +700,6 @@
|
||||
"default_width": 380
|
||||
},
|
||||
"agent": {
|
||||
// Version of this setting.
|
||||
"version": "2",
|
||||
// Whether the agent is enabled.
|
||||
"enabled": true,
|
||||
/// What completion mode to start new threads in, if available. Can be 'normal' or 'max'.
|
||||
@@ -1002,6 +1006,8 @@
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
// Whether to show the project diagnostics button in the status bar.
|
||||
"button": true,
|
||||
// Whether to show warnings or not by default.
|
||||
"include_warnings": true,
|
||||
// Settings for inline diagnostics
|
||||
@@ -1297,21 +1303,22 @@
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
|
||||
"Shell Script": [".env.*"]
|
||||
},
|
||||
// By default use a recent system version of node, or install our own.
|
||||
// You can override this to use a version of node that is not in $PATH with:
|
||||
// {
|
||||
// "node": {
|
||||
// "path": "/path/to/node"
|
||||
// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
|
||||
// }
|
||||
// }
|
||||
// or to ensure Zed always downloads and installs an isolated version of node:
|
||||
// {
|
||||
// "node": {
|
||||
// "ignore_system_version": true,
|
||||
// }
|
||||
// NOTE: changing this setting currently requires restarting Zed.
|
||||
"node": {},
|
||||
// Settings for which version of Node.js and NPM to use when installing
|
||||
// language servers and Copilot.
|
||||
//
|
||||
// Note: changing this setting currently requires restarting Zed.
|
||||
"node": {
|
||||
// By default, Zed will look for `node` and `npm` on your `$PATH`, and use the
|
||||
// existing executables if their version is recent enough. Set this to `true`
|
||||
// to prevent this, and force Zed to always download and install its own
|
||||
// version of Node.
|
||||
"ignore_system_version": false,
|
||||
// You can also specify alternative paths to Node and NPM. If you specify
|
||||
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
|
||||
// `${path}/../npm`.
|
||||
"path": null,
|
||||
"npm_path": null
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
// If you don't want any of these extensions, add this field to your settings
|
||||
@@ -1551,7 +1558,6 @@
|
||||
// Different settings for specific language models.
|
||||
"language_models": {
|
||||
"anthropic": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.anthropic.com"
|
||||
},
|
||||
"google": {
|
||||
@@ -1561,7 +1567,6 @@
|
||||
"api_url": "http://localhost:11434"
|
||||
},
|
||||
"openai": {
|
||||
"version": "1",
|
||||
"api_url": "https://api.openai.com/v1"
|
||||
},
|
||||
"lmstudio": {
|
||||
|
||||
@@ -3,9 +3,10 @@ use crate::context::{AgentContextHandle, RULES_ICON};
|
||||
use crate::context_picker::{ContextPicker, MentionLink};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::message_editor::insert_message_creases;
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, Thread, ThreadError, ThreadEvent,
|
||||
ThreadFeedback,
|
||||
LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback, ThreadSummary,
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, TextThreadStore, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
@@ -327,6 +328,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
|
||||
}
|
||||
}
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
|
||||
|
||||
fn render_markdown_code_block(
|
||||
@@ -485,12 +487,13 @@ fn render_markdown_code_block(
|
||||
.copied_code_block_ids
|
||||
.contains(&(message_id, ix));
|
||||
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, ix))
|
||||
.copied()
|
||||
.unwrap_or(true);
|
||||
let can_expand = metadata.line_count >= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
|
||||
|
||||
let is_expanded = if can_expand {
|
||||
active_thread.read(cx).is_codeblock_expanded(message_id, ix)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let codeblock_header_bg = cx
|
||||
.theme()
|
||||
@@ -511,7 +514,7 @@ fn render_markdown_code_block(
|
||||
.children(label)
|
||||
.child(
|
||||
h_flex()
|
||||
.visible_on_hover("codeblock_container")
|
||||
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
|
||||
.gap_1()
|
||||
.child(
|
||||
IconButton::new(
|
||||
@@ -553,45 +556,38 @@ fn render_markdown_code_block(
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(
|
||||
metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
|
||||
|header| {
|
||||
header.child(
|
||||
IconButton::new(
|
||||
("expand-collapse-code", ix),
|
||||
if is_expanded {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
},
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text(if is_expanded {
|
||||
"Collapse Code"
|
||||
.when(can_expand, |header| {
|
||||
header.child(
|
||||
IconButton::new(
|
||||
("expand-collapse-code", ix),
|
||||
if is_expanded {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
"Expand Code"
|
||||
}))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
let is_expanded = this
|
||||
.expanded_code_blocks
|
||||
.entry((message_id, ix))
|
||||
.or_insert(true);
|
||||
*is_expanded = !*is_expanded;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
IconName::ChevronDown
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
.icon_color(Color::Muted)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::text(if is_expanded {
|
||||
"Collapse Code"
|
||||
} else {
|
||||
"Expand Code"
|
||||
}))
|
||||
.on_click({
|
||||
let active_thread = active_thread.clone();
|
||||
move |_event, _window, cx| {
|
||||
active_thread.update(cx, |this, cx| {
|
||||
this.toggle_codeblock_expanded(message_id, ix);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.group("codeblock_container")
|
||||
.group(CODEBLOCK_CONTAINER_GROUP)
|
||||
.my_2()
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
@@ -599,16 +595,7 @@ fn render_markdown_code_block(
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(codeblock_header)
|
||||
.when(
|
||||
metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
|
||||
|this| {
|
||||
if is_expanded {
|
||||
this.h_full()
|
||||
} else {
|
||||
this.max_h_80()
|
||||
}
|
||||
},
|
||||
)
|
||||
.when(can_expand && !is_expanded, |this| this.max_h_80())
|
||||
}
|
||||
|
||||
fn render_code_language(
|
||||
@@ -827,12 +814,12 @@ impl ActiveThread {
|
||||
self.messages.is_empty()
|
||||
}
|
||||
|
||||
pub fn summary(&self, cx: &App) -> Option<SharedString> {
|
||||
pub fn summary<'a>(&'a self, cx: &'a App) -> &'a ThreadSummary {
|
||||
self.thread.read(cx).summary()
|
||||
}
|
||||
|
||||
pub fn summary_or_default(&self, cx: &App) -> SharedString {
|
||||
self.thread.read(cx).summary_or_default()
|
||||
pub fn regenerate_summary(&self, cx: &mut App) {
|
||||
self.thread.update(cx, |thread, cx| thread.summarize(cx))
|
||||
}
|
||||
|
||||
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
@@ -1138,11 +1125,7 @@ impl ActiveThread {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Agent Panel".into());
|
||||
let title = self.thread.read(cx).summary().unwrap_or("Agent Panel");
|
||||
|
||||
match AssistantSettings::get_global(cx).notify_when_agent_waiting {
|
||||
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||
@@ -1272,6 +1255,7 @@ impl ActiveThread {
|
||||
&mut self,
|
||||
message_id: MessageId,
|
||||
message_segments: &[MessageSegment],
|
||||
message_creases: &[MessageCrease],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1291,6 +1275,7 @@ impl ActiveThread {
|
||||
);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(message_text.clone(), window, cx);
|
||||
insert_message_creases(editor, message_creases, &self.context_store, window, cx);
|
||||
editor.focus_handle(cx).focus(window);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
});
|
||||
@@ -1467,10 +1452,12 @@ impl ActiveThread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some((_, state)) = self.editing_message.as_mut() {
|
||||
if state.context_picker_menu_handle.is_deployed() {
|
||||
cx.propagate();
|
||||
} else {
|
||||
state.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1743,6 +1730,7 @@ impl ActiveThread {
|
||||
let Some(message) = self.thread.read(cx).message(message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
let message_creases = message.creases.clone();
|
||||
|
||||
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
|
||||
return Empty.into_any();
|
||||
@@ -2034,6 +2022,7 @@ impl ActiveThread {
|
||||
this.start_editing_message(
|
||||
message_id,
|
||||
&message_segments,
|
||||
&message_creases,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -2238,7 +2227,7 @@ impl ActiveThread {
|
||||
// Backdrop to dim out the whole thread below the editing user message
|
||||
parent.relative().child(
|
||||
div()
|
||||
.occlude()
|
||||
.stop_mouse_events_except_scroll()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
@@ -2357,19 +2346,19 @@ impl ActiveThread {
|
||||
let editor_bg = cx.theme().colors().editor_background;
|
||||
|
||||
move |el, range, metadata, _, cx| {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, range.start))
|
||||
.copied()
|
||||
.unwrap_or(true);
|
||||
|
||||
if is_expanded
|
||||
|| metadata.line_count
|
||||
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
|
||||
{
|
||||
let can_expand = metadata.line_count
|
||||
>= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
|
||||
if !can_expand {
|
||||
return el;
|
||||
}
|
||||
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.is_codeblock_expanded(message_id, range.start);
|
||||
if is_expanded {
|
||||
return el;
|
||||
}
|
||||
|
||||
el.child(
|
||||
div()
|
||||
.absolute()
|
||||
@@ -2395,6 +2384,7 @@ impl ActiveThread {
|
||||
markdown_element.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: true,
|
||||
},
|
||||
)
|
||||
@@ -2714,6 +2704,7 @@ impl ActiveThread {
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
@@ -2744,6 +2735,7 @@ impl ActiveThread {
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
@@ -3380,6 +3372,21 @@ impl ActiveThread {
|
||||
.log_err();
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool {
|
||||
self.expanded_code_blocks
|
||||
.get(&(message_id, ix))
|
||||
.copied()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn toggle_codeblock_expanded(&mut self, message_id: MessageId, ix: usize) {
|
||||
let is_expanded = self
|
||||
.expanded_code_blocks
|
||||
.entry((message_id, ix))
|
||||
.or_insert(false);
|
||||
*is_expanded = !*is_expanded;
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActiveThreadEvent {
|
||||
@@ -3434,10 +3441,7 @@ pub(crate) fn open_active_thread_as_markdown(
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let thread = thread.read(cx);
|
||||
let markdown = thread.to_markdown(cx)?;
|
||||
let thread_summary = thread
|
||||
.summary()
|
||||
.map(|summary| summary.to_string())
|
||||
.unwrap_or_else(|| "Thread".to_string());
|
||||
let thread_summary = thread.summary().or_default().to_string();
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
expanded_context_server_tools: HashMap<ContextServerId, bool>,
|
||||
expanded_provider_configurations: HashMap<LanguageModelProviderId, bool>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
@@ -78,6 +79,7 @@ impl AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
@@ -96,6 +98,7 @@ impl AgentConfiguration {
|
||||
|
||||
fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
|
||||
self.configuration_views_by_provider.remove(provider_id);
|
||||
self.expanded_provider_configurations.remove(provider_id);
|
||||
}
|
||||
|
||||
fn add_provider_configuration_view(
|
||||
@@ -135,9 +138,14 @@ impl AgentConfiguration {
|
||||
.get(&provider.id())
|
||||
.cloned();
|
||||
|
||||
let is_expanded = self
|
||||
.expanded_provider_configurations
|
||||
.get(&provider.id())
|
||||
.copied()
|
||||
.unwrap_or(true);
|
||||
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pb_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
@@ -152,32 +160,59 @@ impl AgentConfiguration {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large))
|
||||
.when(provider.is_authenticated(cx) && !is_expanded, |parent| {
|
||||
parent.child(Icon::new(IconName::Check).color(Color::Success))
|
||||
}),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_this, _event, _window, cx| {
|
||||
cx.emit(AssistantConfigurationEvent::NewThread(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let provider = provider.clone();
|
||||
move |_this, _event, _window, cx| {
|
||||
cx.emit(AssistantConfigurationEvent::NewThread(
|
||||
provider.clone(),
|
||||
))
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Disclosure::new(
|
||||
SharedString::from(format!(
|
||||
"provider-disclosure-{provider_id}"
|
||||
)),
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.on_click(cx.listener({
|
||||
let provider_id = provider.id().clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
let is_open = this
|
||||
.expanded_provider_configurations
|
||||
.entry(provider_id.clone())
|
||||
.or_insert(true);
|
||||
|
||||
*is_open = !*is_open;
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.map(|parent| match configuration_view {
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(div().child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
@@ -387,6 +422,7 @@ impl AgentConfiguration {
|
||||
.unwrap_or(ContextServerStatus::Stopped);
|
||||
|
||||
let is_running = matches!(server_status, ContextServerStatus::Running);
|
||||
let item_id = SharedString::from(context_server_id.0.clone());
|
||||
|
||||
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
|
||||
Some(error)
|
||||
@@ -408,9 +444,38 @@ impl AgentConfiguration {
|
||||
let tool_count = tools.len();
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let success_color = Color::Success.color(cx);
|
||||
|
||||
let (status_indicator, tooltip_text) = match server_status {
|
||||
ContextServerStatus::Starting => (
|
||||
Indicator::dot()
|
||||
.color(Color::Success)
|
||||
.with_animation(
|
||||
SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.)),
|
||||
move |this, delta| this.color(success_color.alpha(delta).into()),
|
||||
)
|
||||
.into_any_element(),
|
||||
"Server is starting.",
|
||||
),
|
||||
ContextServerStatus::Running => (
|
||||
Indicator::dot().color(Color::Success).into_any_element(),
|
||||
"Server is running.",
|
||||
),
|
||||
ContextServerStatus::Error(_) => (
|
||||
Indicator::dot().color(Color::Error).into_any_element(),
|
||||
"Server has an error.",
|
||||
),
|
||||
ContextServerStatus::Stopped => (
|
||||
Indicator::dot().color(Color::Muted).into_any_element(),
|
||||
"Server is stopped.",
|
||||
),
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id(SharedString::from(context_server_id.0.clone()))
|
||||
.id(item_id.clone())
|
||||
.border_1()
|
||||
.rounded_md()
|
||||
.border_color(border_color)
|
||||
@@ -445,35 +510,12 @@ impl AgentConfiguration {
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(match server_status {
|
||||
ContextServerStatus::Starting => {
|
||||
let color = Color::Success.color(cx);
|
||||
Indicator::dot()
|
||||
.color(Color::Success)
|
||||
.with_animation(
|
||||
SharedString::from(format!(
|
||||
"{}-starting",
|
||||
context_server_id.0.clone(),
|
||||
)),
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 1.)),
|
||||
move |this, delta| {
|
||||
this.color(color.alpha(delta).into())
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
ContextServerStatus::Running => {
|
||||
Indicator::dot().color(Color::Success).into_any_element()
|
||||
}
|
||||
ContextServerStatus::Error(_) => {
|
||||
Indicator::dot().color(Color::Error).into_any_element()
|
||||
}
|
||||
ContextServerStatus::Stopped => {
|
||||
Indicator::dot().color(Color::Muted).into_any_element()
|
||||
}
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.id(item_id.clone())
|
||||
.tooltip(Tooltip::text(tooltip_text))
|
||||
.child(status_indicator),
|
||||
)
|
||||
.child(Label::new(context_server_id.0.clone()).ml_0p5())
|
||||
.when(is_running, |this| {
|
||||
this.child(
|
||||
|
||||
@@ -274,42 +274,35 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
let server_id = server_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
move |settings: &mut AssistantSettingsContent, _cx| {
|
||||
settings
|
||||
.v2_setting(|v2_settings| {
|
||||
let profiles = v2_settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
enable_all_context_servers: Some(
|
||||
default_profile.enable_all_context_servers,
|
||||
),
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
let profile = profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
enable_all_context_servers: Some(
|
||||
default_profile.enable_all_context_servers,
|
||||
),
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
|
||||
if let Some(server_id) = server_id {
|
||||
let preset = profile.context_servers.entry(server_id).or_default();
|
||||
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
} else {
|
||||
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
if let Some(server_id) = server_id {
|
||||
let preset = profile.context_servers.entry(server_id).or_default();
|
||||
*preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
} else {
|
||||
*profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use crate::{
|
||||
Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel,
|
||||
};
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent};
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
@@ -11,8 +9,9 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
|
||||
WeakEntity, Window, percentage, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
@@ -25,6 +24,7 @@ use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use util::ResultExt;
|
||||
@@ -215,11 +215,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Agent Changes".into());
|
||||
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
@@ -469,11 +465,7 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("Agent Changes".into());
|
||||
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
@@ -978,9 +970,20 @@ impl ToolbarItemView for AgentDiffToolbar {
|
||||
|
||||
impl Render for AgentDiffToolbar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let generating_label = div()
|
||||
.w(rems_from_px(110.)) // Arbitrary size so the label doesn't dance around
|
||||
.child(AnimatedLabel::new("Generating"))
|
||||
let spinner_icon = div()
|
||||
.px_0p5()
|
||||
.id("generating")
|
||||
.tooltip(Tooltip::text("Generating Changes…"))
|
||||
.child(
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"load_circle",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let Some(active_item) = self.active_item.as_ref() else {
|
||||
@@ -997,7 +1000,7 @@ impl Render for AgentDiffToolbar {
|
||||
|
||||
let content = match state {
|
||||
EditorState::Idle => return Empty.into_any(),
|
||||
EditorState::Generating => vec![generating_label],
|
||||
EditorState::Generating => vec![spinner_icon],
|
||||
EditorState::Reviewing => vec![
|
||||
h_flex()
|
||||
.child(
|
||||
@@ -1115,7 +1118,7 @@ impl Render for AgentDiffToolbar {
|
||||
|
||||
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
|
||||
if is_generating {
|
||||
return div().px_2().child(generating_label).into_any();
|
||||
return div().px_2().child(spinner_icon).into_any();
|
||||
}
|
||||
|
||||
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();
|
||||
|
||||
@@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
|
||||
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
|
||||
render_remaining_tokens,
|
||||
ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
|
||||
make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
@@ -46,7 +46,9 @@ use ui::{
|
||||
};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
|
||||
use workspace::{
|
||||
CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
|
||||
};
|
||||
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
|
||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
@@ -55,10 +57,10 @@ 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::history_store::{HistoryEntry, HistoryStore, RecentEntry};
|
||||
use crate::history_store::{HistoryStore, RecentEntry};
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, ThreadSummary, TokenUsageRatio};
|
||||
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::AgentOnboardingModal;
|
||||
use crate::{
|
||||
@@ -194,7 +196,7 @@ impl ActiveView {
|
||||
}
|
||||
|
||||
pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
|
||||
let summary = thread.read(cx).summary_or_default();
|
||||
let summary = thread.read(cx).summary().or_default();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
@@ -216,7 +218,7 @@ impl ActiveView {
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
if editor.read(cx).text(cx).is_empty() {
|
||||
let summary = thread.read(cx).summary_or_default();
|
||||
let summary = thread.read(cx).summary().or_default();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(summary, window, cx);
|
||||
@@ -231,7 +233,7 @@ impl ActiveView {
|
||||
let editor = editor.clone();
|
||||
move |thread, event, window, cx| match event {
|
||||
ThreadEvent::SummaryGenerated => {
|
||||
let summary = thread.read(cx).summary_or_default();
|
||||
let summary = thread.read(cx).summary().or_default();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(summary, window, cx);
|
||||
@@ -294,7 +296,8 @@ impl ActiveView {
|
||||
.read(cx)
|
||||
.context()
|
||||
.read(cx)
|
||||
.summary_or_default();
|
||||
.summary()
|
||||
.or_default();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(summary, window, cx);
|
||||
@@ -309,7 +312,7 @@ impl ActiveView {
|
||||
let editor = editor.clone();
|
||||
move |assistant_context, event, window, cx| match event {
|
||||
ContextEvent::SummaryGenerated => {
|
||||
let summary = assistant_context.read(cx).summary_or_default();
|
||||
let summary = assistant_context.read(cx).summary().or_default();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(summary, window, cx);
|
||||
@@ -356,11 +359,13 @@ pub struct AgentPanel {
|
||||
previous_view: Option<ActiveView>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
history: Entity<ThreadHistory>,
|
||||
hovered_recent_history_item: Option<usize>,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu: Option<Entity<ContextMenu>>,
|
||||
width: Option<Pixels>,
|
||||
height: Option<Pixels>,
|
||||
zoomed: bool,
|
||||
pending_serialization: Option<Task<Result<()>>>,
|
||||
hide_trial_upsell: bool,
|
||||
_trial_markdown: Entity<Markdown>,
|
||||
@@ -696,11 +701,13 @@ impl AgentPanel {
|
||||
previous_view: None,
|
||||
history_store: history_store.clone(),
|
||||
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
|
||||
hovered_recent_history_item: None,
|
||||
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu: None,
|
||||
width: None,
|
||||
height: None,
|
||||
zoomed: false,
|
||||
pending_serialization: None,
|
||||
hide_trial_upsell: false,
|
||||
_trial_markdown: trial_markdown,
|
||||
@@ -1142,6 +1149,17 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.zoomed {
|
||||
cx.emit(PanelEvent::ZoomOut);
|
||||
} else {
|
||||
if !self.focus_handle(cx).contains_focused(window, cx) {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
cx.emit(PanelEvent::ZoomIn);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_agent_diff(
|
||||
&mut self,
|
||||
_: &OpenAgentDiff,
|
||||
@@ -1414,6 +1432,15 @@ impl Panel for AgentPanel {
|
||||
fn enabled(&self, cx: &App) -> bool {
|
||||
AssistantSettings::get_global(cx).enabled
|
||||
}
|
||||
|
||||
fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
|
||||
self.zoomed
|
||||
}
|
||||
|
||||
fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.zoomed = zoomed;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
@@ -1426,23 +1453,45 @@ impl AgentPanel {
|
||||
..
|
||||
} => {
|
||||
let active_thread = self.thread.read(cx);
|
||||
let is_empty = active_thread.is_empty();
|
||||
|
||||
let summary = active_thread.summary(cx);
|
||||
|
||||
if is_empty {
|
||||
Label::new(Thread::DEFAULT_SUMMARY.clone())
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
} else if summary.is_none() {
|
||||
Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.into_any_element()
|
||||
let state = if active_thread.is_empty() {
|
||||
&ThreadSummary::Pending
|
||||
} else {
|
||||
div()
|
||||
active_thread.summary(cx)
|
||||
};
|
||||
|
||||
match state {
|
||||
ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
|
||||
.truncate()
|
||||
.into_any_element(),
|
||||
ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
|
||||
.truncate()
|
||||
.into_any_element(),
|
||||
ThreadSummary::Ready(_) => div()
|
||||
.w_full()
|
||||
.child(change_title_editor.clone())
|
||||
.into_any_element()
|
||||
.into_any_element(),
|
||||
ThreadSummary::Error => h_flex()
|
||||
.w_full()
|
||||
.child(change_title_editor.clone())
|
||||
.child(
|
||||
ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
|
||||
.on_click({
|
||||
let active_thread = self.thread.clone();
|
||||
move |_, _window, cx| {
|
||||
active_thread.update(cx, |thread, cx| {
|
||||
thread.regenerate_summary(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.tooltip(move |_window, cx| {
|
||||
cx.new(|_| {
|
||||
Tooltip::new("Failed to generate title")
|
||||
.meta("Click to try again")
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor {
|
||||
@@ -1450,14 +1499,13 @@ impl AgentPanel {
|
||||
context_editor,
|
||||
..
|
||||
} => {
|
||||
let context_editor = context_editor.read(cx);
|
||||
let summary = context_editor.context().read(cx).summary();
|
||||
let summary = context_editor.read(cx).context().read(cx).summary();
|
||||
|
||||
match summary {
|
||||
None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
|
||||
ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
|
||||
.truncate()
|
||||
.into_any_element(),
|
||||
Some(summary) => {
|
||||
ContextSummary::Content(summary) => {
|
||||
if summary.done {
|
||||
div()
|
||||
.w_full()
|
||||
@@ -1469,6 +1517,28 @@ impl AgentPanel {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
ContextSummary::Error => h_flex()
|
||||
.w_full()
|
||||
.child(title_editor.clone())
|
||||
.child(
|
||||
ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
|
||||
.on_click({
|
||||
let context_editor = context_editor.clone();
|
||||
move |_, _window, cx| {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
context_editor.regenerate_summary(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.tooltip(move |_window, cx| {
|
||||
cx.new(|_| {
|
||||
Tooltip::new("Failed to generate title")
|
||||
.meta("Click to try again")
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
ActiveView::History => Label::new("History").truncate().into_any_element(),
|
||||
@@ -1578,6 +1648,12 @@ impl AgentPanel {
|
||||
}),
|
||||
);
|
||||
|
||||
let zoom_in_label = if self.is_zoomed(window, cx) {
|
||||
"Zoom Out"
|
||||
} else {
|
||||
"Zoom In"
|
||||
};
|
||||
|
||||
let agent_extra_menu = PopoverMenu::new("agent-options-menu")
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("agent-options-menu", IconName::Ellipsis)
|
||||
@@ -1664,7 +1740,8 @@ impl AgentPanel {
|
||||
|
||||
menu = menu
|
||||
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration));
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.action(zoom_in_label, Box::new(ToggleZoom));
|
||||
menu
|
||||
}))
|
||||
});
|
||||
@@ -2212,7 +2289,7 @@ impl AgentPanel {
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(
|
||||
Label::new("Past Interactions")
|
||||
Label::new("Recent")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
@@ -2237,18 +2314,20 @@ impl AgentPanel {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.children(
|
||||
recent_history.into_iter().map(|entry| {
|
||||
recent_history.into_iter().enumerate().map(|(index, entry)| {
|
||||
// TODO: Add keyboard navigation.
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
PastThread::new(thread, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
|
||||
.into_any_element()
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
PastContext::new(context, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
let is_hovered = self.hovered_recent_history_item == Some(index);
|
||||
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
|
||||
.hovered(is_hovered)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_recent_history_item = Some(index);
|
||||
} else if this.hovered_recent_history_item == Some(index) {
|
||||
this.hovered_recent_history_item = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
)
|
||||
@@ -2776,6 +2855,7 @@ impl Render for AgentPanel {
|
||||
.on_action(cx.listener(Self::increase_font_size))
|
||||
.on_action(cx.listener(Self::decrease_font_size))
|
||||
.on_action(cx.listener(Self::reset_font_size))
|
||||
.on_action(cx.listener(Self::toggle_zoom))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.children(self.render_trial_upsell(window, cx))
|
||||
.map(|parent| match &self.active_view {
|
||||
|
||||
@@ -586,10 +586,7 @@ impl ThreadContextHandle {
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or_else(|| "New thread".into())
|
||||
self.thread.read(cx).summary().or_default()
|
||||
}
|
||||
|
||||
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
@@ -597,9 +594,7 @@ impl ThreadContextHandle {
|
||||
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
|
||||
let title = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| {
|
||||
thread.summary().unwrap_or_else(|| "New thread".into())
|
||||
})
|
||||
.read_with(cx, |thread, _cx| thread.summary().or_default())
|
||||
.ok()?;
|
||||
let context = AgentContext::Thread(ThreadContext {
|
||||
title,
|
||||
@@ -642,7 +637,7 @@ impl TextThreadContextHandle {
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.context.read(cx).summary_or_default()
|
||||
self.context.read(cx).summary().or_default()
|
||||
}
|
||||
|
||||
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
@@ -830,23 +825,20 @@ pub fn load_context(
|
||||
prompt_store: &Option<Entity<PromptStore>>,
|
||||
cx: &mut App,
|
||||
) -> Task<ContextLoadResult> {
|
||||
let mut load_tasks = Vec::new();
|
||||
|
||||
for context in contexts.iter().cloned() {
|
||||
match context {
|
||||
AgentContextHandle::File(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::Directory(context) => {
|
||||
load_tasks.push(context.load(project.clone(), cx))
|
||||
}
|
||||
AgentContextHandle::Symbol(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::Selection(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::FetchedUrl(context) => load_tasks.push(context.load()),
|
||||
AgentContextHandle::Thread(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::TextThread(context) => load_tasks.push(context.load(cx)),
|
||||
AgentContextHandle::Rules(context) => load_tasks.push(context.load(prompt_store, cx)),
|
||||
AgentContextHandle::Image(context) => load_tasks.push(context.load(cx)),
|
||||
}
|
||||
}
|
||||
let load_tasks: Vec<_> = contexts
|
||||
.into_iter()
|
||||
.map(|context| match context {
|
||||
AgentContextHandle::File(context) => context.load(cx),
|
||||
AgentContextHandle::Directory(context) => context.load(project.clone(), cx),
|
||||
AgentContextHandle::Symbol(context) => context.load(cx),
|
||||
AgentContextHandle::Selection(context) => context.load(cx),
|
||||
AgentContextHandle::FetchedUrl(context) => context.load(),
|
||||
AgentContextHandle::Thread(context) => context.load(cx),
|
||||
AgentContextHandle::TextThread(context) => context.load(cx),
|
||||
AgentContextHandle::Rules(context) => context.load(prompt_store, cx),
|
||||
AgentContextHandle::Image(context) => context.load(cx),
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let load_results = future::join_all(load_tasks).await;
|
||||
|
||||
@@ -381,6 +381,16 @@ impl ContextPicker {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
|
||||
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.mode {
|
||||
ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| {
|
||||
entity.select_first(&Default::default(), window, cx)
|
||||
}),
|
||||
// Other variants already select their first entry on open automatically
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn recent_menu_item(
|
||||
&self,
|
||||
context_picker: Entity<ContextPicker>,
|
||||
|
||||
@@ -160,7 +160,7 @@ impl ContextStrip {
|
||||
}
|
||||
|
||||
Some(SuggestedContext::Thread {
|
||||
name: active_thread.summary_or_default(),
|
||||
name: active_thread.summary().or_default(),
|
||||
thread: weak_active_thread,
|
||||
})
|
||||
} else if let Some(active_context_editor) = panel.active_context_editor() {
|
||||
@@ -174,7 +174,7 @@ impl ContextStrip {
|
||||
}
|
||||
|
||||
Some(SuggestedContext::TextThread {
|
||||
name: context.summary_or_default(),
|
||||
name: context.summary().or_default(),
|
||||
context: weak_context,
|
||||
})
|
||||
} else {
|
||||
@@ -420,12 +420,25 @@ impl Render for ContextStrip {
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new("context-picker")
|
||||
.menu(move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
.menu({
|
||||
let context_picker = context_picker.clone();
|
||||
move |window, cx| {
|
||||
context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
|
||||
Some(context_picker.clone())
|
||||
Some(context_picker.clone())
|
||||
}
|
||||
})
|
||||
.on_open({
|
||||
let context_picker = context_picker.downgrade();
|
||||
Rc::new(move |window, cx| {
|
||||
context_picker
|
||||
.update(cx, |context_picker, cx| {
|
||||
context_picker.select_first(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("add-context", IconName::Plus)
|
||||
|
||||
@@ -75,7 +75,7 @@ impl Default for DebugAccountState {
|
||||
Self {
|
||||
enabled: false,
|
||||
trial_expired: false,
|
||||
plan: Plan::Free,
|
||||
plan: Plan::ZedFree,
|
||||
custom_prompt_usage: RequestUsage {
|
||||
limit: UsageLimit::Unlimited,
|
||||
amount: 0,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
use std::{collections::VecDeque, path::Path, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
|
||||
@@ -34,6 +34,20 @@ impl HistoryEntry {
|
||||
HistoryEntry::Context(context) => context.mtime.to_utc(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> HistoryEntryId {
|
||||
match self {
|
||||
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
|
||||
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic identifier for a history entry.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum HistoryEntryId {
|
||||
Thread(ThreadId),
|
||||
Context(Arc<Path>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -57,8 +71,8 @@ impl Eq for RecentEntry {}
|
||||
impl RecentEntry {
|
||||
pub(crate) fn summary(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
RecentEntry::Thread(_, thread) => thread.read(cx).summary_or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary_or_default(),
|
||||
RecentEntry::Thread(_, thread) => thread.read(cx).summary().or_default(),
|
||||
RecentEntry::Context(context) => context.read(cx).summary().or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,13 +338,27 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
|
||||
(
|
||||
editor.snapshot(window, cx),
|
||||
editor.selections.all::<Point>(cx),
|
||||
)
|
||||
let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
let newest_selection = editor.selections.newest::<Point>(cx);
|
||||
(editor.snapshot(window, cx), selections, newest_selection)
|
||||
});
|
||||
|
||||
// Check if there is already an inline assistant that contains the
|
||||
// newest selection, if there is, focus it
|
||||
if let Some(editor_assists) = self.assists_by_editor.get(&editor.downgrade()) {
|
||||
for assist_id in &editor_assists.assist_ids {
|
||||
let assist = &self.assists[assist_id];
|
||||
let range = assist.range.to_point(&snapshot.buffer_snapshot);
|
||||
if range.start.row <= newest_selection.start.row
|
||||
&& newest_selection.end.row <= range.end.row
|
||||
{
|
||||
self.focus_assist(*assist_id, window, cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut selections = Vec::<Selection<Point>>::new();
|
||||
let mut newest_selection = None;
|
||||
for mut selection in initial_selections {
|
||||
|
||||
@@ -200,7 +200,13 @@ impl MessageEditor {
|
||||
});
|
||||
|
||||
let profile_selector = cx.new(|cx| {
|
||||
ProfileSelector::new(thread.clone(), thread_store, editor.focus_handle(cx), cx)
|
||||
ProfileSelector::new(
|
||||
fs,
|
||||
thread.clone(),
|
||||
thread_store,
|
||||
editor.focus_handle(cx),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -392,9 +398,11 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.context_picker_menu_handle.is_deployed() {
|
||||
cx.propagate();
|
||||
} else {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,11 +1085,11 @@ impl MessageEditor {
|
||||
let plan = user_store
|
||||
.current_plan()
|
||||
.map(|plan| match plan {
|
||||
Plan::Free => zed_llm_client::Plan::Free,
|
||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
})
|
||||
.unwrap_or(zed_llm_client::Plan::Free);
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
let usage = self.thread.read(cx).last_usage().or_else(|| {
|
||||
maybe!({
|
||||
let amount = user_store.model_request_usage_amount()?;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileId, AssistantDockPosition, 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::*,
|
||||
@@ -15,6 +18,7 @@ use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: GroupedAgentProfiles,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread: Entity<Thread>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
@@ -24,6 +28,7 @@ pub struct ProfileSelector {
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread: Entity<Thread>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -35,6 +40,7 @@ impl ProfileSelector {
|
||||
|
||||
Self {
|
||||
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
|
||||
fs,
|
||||
thread,
|
||||
thread_store,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
@@ -95,7 +101,7 @@ impl ProfileSelector {
|
||||
profile_id: AgentProfileId,
|
||||
profile: &AgentProfile,
|
||||
settings: &AssistantSettings,
|
||||
cx: &App,
|
||||
_cx: &App,
|
||||
) -> ContextMenuEntry {
|
||||
let documentation = match profile.name.to_lowercase().as_str() {
|
||||
builtin_profiles::WRITE => Some("Get help to write anything."),
|
||||
@@ -104,12 +110,8 @@ impl ProfileSelector {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let current_profile_id = self.thread.read(cx).configured_profile_id();
|
||||
|
||||
let entry = ContextMenuEntry::new(profile.name.clone()).toggleable(
|
||||
IconPosition::End,
|
||||
Some(profile_id.clone()) == current_profile_id,
|
||||
);
|
||||
let entry = ContextMenuEntry::new(profile.name.clone())
|
||||
.toggleable(IconPosition::End, profile_id == settings.default_profile);
|
||||
|
||||
let entry = if let Some(doc_text) = documentation {
|
||||
entry.documentation_aside(documentation_side(settings.dock), move |_| {
|
||||
@@ -120,13 +122,15 @@ impl ProfileSelector {
|
||||
};
|
||||
|
||||
entry.handler({
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
let profile_id = profile_id.clone();
|
||||
let thread = self.thread.clone();
|
||||
|
||||
move |_window, cx| {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_configured_profile_id(Some(profile_id.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
|
||||
@@ -142,12 +146,8 @@ 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_id = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.configured_profile_id()
|
||||
.unwrap_or(settings.default_profile.clone());
|
||||
let profile = settings.profiles.get(&profile_id).cloned();
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
|
||||
let selected_profile = profile
|
||||
.map(|profile| profile.name.clone())
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings, CompletionMode};
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -36,7 +36,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use thiserror::Error;
|
||||
use ui::Window;
|
||||
use util::{ResultExt as _, TryFutureExt as _, post_inc};
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
|
||||
@@ -324,7 +324,7 @@ pub enum QueueState {
|
||||
pub struct Thread {
|
||||
id: ThreadId,
|
||||
updated_at: DateTime<Utc>,
|
||||
summary: Option<SharedString>,
|
||||
summary: ThreadSummary,
|
||||
pending_summary: Task<Option<()>>,
|
||||
detailed_summary_task: Task<Option<()>>,
|
||||
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
|
||||
@@ -359,7 +359,33 @@ pub struct Thread {
|
||||
>,
|
||||
remaining_turns: u32,
|
||||
configured_model: Option<ConfiguredModel>,
|
||||
configured_profile_id: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ThreadSummary {
|
||||
Pending,
|
||||
Generating,
|
||||
Ready(SharedString),
|
||||
Error,
|
||||
}
|
||||
|
||||
impl ThreadSummary {
|
||||
pub const DEFAULT: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn or_default(&self) -> SharedString {
|
||||
self.unwrap_or(Self::DEFAULT)
|
||||
}
|
||||
|
||||
pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
|
||||
self.ready().unwrap_or_else(|| message.into())
|
||||
}
|
||||
|
||||
pub fn ready(&self) -> Option<SharedString> {
|
||||
match self {
|
||||
ThreadSummary::Ready(summary) => Some(summary.clone()),
|
||||
ThreadSummary::Pending | ThreadSummary::Generating | ThreadSummary::Error => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -380,13 +406,11 @@ 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 configured_profile_id = assistant_settings.default_profile.clone();
|
||||
|
||||
Self {
|
||||
id: ThreadId::new(),
|
||||
updated_at: Utc::now(),
|
||||
summary: None,
|
||||
summary: ThreadSummary::Pending,
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
@@ -424,7 +448,6 @@ impl Thread {
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_profile_id: Some(configured_profile_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,12 +495,10 @@ impl Thread {
|
||||
.completion_mode
|
||||
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
|
||||
|
||||
let configured_profile_id = serialized.profile.clone();
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: serialized.updated_at,
|
||||
summary: Some(serialized.summary),
|
||||
summary: ThreadSummary::Ready(serialized.summary),
|
||||
pending_summary: Task::ready(None),
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
@@ -547,7 +568,6 @@ impl Thread {
|
||||
request_callback: None,
|
||||
remaining_turns: u32::MAX,
|
||||
configured_model,
|
||||
configured_profile_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,10 +599,6 @@ impl Thread {
|
||||
self.last_prompt_id = PromptId::new();
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<SharedString> {
|
||||
self.summary.clone()
|
||||
}
|
||||
|
||||
pub fn project_context(&self) -> SharedProjectContext {
|
||||
self.project_context.clone()
|
||||
}
|
||||
@@ -603,39 +619,25 @@ impl Thread {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn configured_profile_id(&self) -> Option<AgentProfileId> {
|
||||
self.configured_profile_id.clone()
|
||||
}
|
||||
|
||||
pub fn set_configured_profile_id(
|
||||
&mut self,
|
||||
id: Option<AgentProfileId>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.configured_profile_id = id;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
self.summary.clone().unwrap_or(Self::DEFAULT_SUMMARY)
|
||||
pub fn summary(&self) -> &ThreadSummary {
|
||||
&self.summary
|
||||
}
|
||||
|
||||
pub fn set_summary(&mut self, new_summary: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
let Some(current_summary) = &self.summary else {
|
||||
// Don't allow setting summary until generated
|
||||
return;
|
||||
let current_summary = match &self.summary {
|
||||
ThreadSummary::Pending | ThreadSummary::Generating => return,
|
||||
ThreadSummary::Ready(summary) => summary,
|
||||
ThreadSummary::Error => &ThreadSummary::DEFAULT,
|
||||
};
|
||||
|
||||
let mut new_summary = new_summary.into();
|
||||
|
||||
if new_summary.is_empty() {
|
||||
new_summary = Self::DEFAULT_SUMMARY;
|
||||
new_summary = ThreadSummary::DEFAULT;
|
||||
}
|
||||
|
||||
if current_summary != &new_summary {
|
||||
self.summary = Some(new_summary);
|
||||
self.summary = ThreadSummary::Ready(new_summary);
|
||||
cx.emit(ThreadEvent::SummaryChanged);
|
||||
}
|
||||
}
|
||||
@@ -1049,7 +1051,7 @@ impl Thread {
|
||||
let initial_project_snapshot = initial_project_snapshot.await;
|
||||
this.read_with(cx, |this, cx| SerializedThread {
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
summary: this.summary_or_default(),
|
||||
summary: this.summary().or_default(),
|
||||
updated_at: this.updated_at(),
|
||||
messages: this
|
||||
.messages()
|
||||
@@ -1120,7 +1122,6 @@ impl Thread {
|
||||
provider: model.provider.id().0.to_string(),
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
profile: this.configured_profile_id.clone(),
|
||||
completion_mode: Some(this.completion_mode),
|
||||
})
|
||||
})
|
||||
@@ -1646,7 +1647,7 @@ impl Thread {
|
||||
|
||||
// If there is a response without tool use, summarize the message. Otherwise,
|
||||
// allow two tool uses before summarizing.
|
||||
if thread.summary.is_none()
|
||||
if matches!(thread.summary, ThreadSummary::Pending)
|
||||
&& thread.messages.len() >= 2
|
||||
&& (!thread.has_pending_tool_uses() || thread.messages.len() >= 6)
|
||||
{
|
||||
@@ -1760,6 +1761,7 @@ impl Thread {
|
||||
|
||||
pub fn summarize(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
|
||||
println!("No thread summary model");
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1774,13 +1776,17 @@ impl Thread {
|
||||
|
||||
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
|
||||
|
||||
self.summary = ThreadSummary::Generating;
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let result = async {
|
||||
let mut messages = model.model.stream_completion(request, &cx).await?;
|
||||
|
||||
let mut new_summary = String::new();
|
||||
while let Some(event) = messages.next().await {
|
||||
let event = event?;
|
||||
let Ok(event) = event else {
|
||||
continue;
|
||||
};
|
||||
let text = match event {
|
||||
LanguageModelCompletionEvent::Text(text) => text,
|
||||
LanguageModelCompletionEvent::StatusUpdate(
|
||||
@@ -1806,18 +1812,29 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
if !new_summary.is_empty() {
|
||||
this.summary = Some(new_summary.into());
|
||||
}
|
||||
|
||||
cx.emit(ThreadEvent::SummaryGenerated);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
anyhow::Ok(new_summary)
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
match result {
|
||||
Ok(new_summary) => {
|
||||
if new_summary.is_empty() {
|
||||
this.summary = ThreadSummary::Error;
|
||||
} else {
|
||||
this.summary = ThreadSummary::Ready(new_summary.into());
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
this.summary = ThreadSummary::Error;
|
||||
log::error!("Failed to generate thread summary: {}", err);
|
||||
}
|
||||
}
|
||||
cx.emit(ThreadEvent::SummaryGenerated);
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2224,7 +2241,7 @@ impl Thread {
|
||||
.read(cx)
|
||||
.enabled_tools(cx)
|
||||
.iter()
|
||||
.map(|tool| tool.name().to_string())
|
||||
.map(|tool| tool.name())
|
||||
.collect();
|
||||
|
||||
self.message_feedback.insert(message_id, feedback);
|
||||
@@ -2427,9 +2444,8 @@ impl Thread {
|
||||
pub fn to_markdown(&self, cx: &App) -> Result<String> {
|
||||
let mut markdown = Vec::new();
|
||||
|
||||
if let Some(summary) = self.summary() {
|
||||
writeln!(markdown, "# {summary}\n")?;
|
||||
};
|
||||
let summary = self.summary().or_default();
|
||||
writeln!(markdown, "# {summary}\n")?;
|
||||
|
||||
for message in self.messages() {
|
||||
writeln!(
|
||||
@@ -2746,7 +2762,7 @@ mod tests {
|
||||
use assistant_tool::ToolRegistry;
|
||||
use editor::EditorSettings;
|
||||
use gpui::TestAppContext;
|
||||
use language_model::fake_provider::FakeLanguageModel;
|
||||
use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider};
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
@@ -3247,6 +3263,196 @@ fn main() {{
|
||||
assert_eq!(request.temperature, None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Initial state should be pending
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Pending));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
|
||||
// Manually setting the summary should not be allowed in this state
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("This should not work", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Pending));
|
||||
});
|
||||
|
||||
// Send a message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
// Should start generating summary when there are >= 2 messages
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(*thread.summary(), ThreadSummary::Generating);
|
||||
});
|
||||
|
||||
// Should not be able to set the summary while generating
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("This should not work either", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Generating));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Brief".into());
|
||||
fake_model.stream_last_completion_response(" Introduction".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Summary should be set
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), "Brief Introduction");
|
||||
});
|
||||
|
||||
// Now we should be able to set a summary
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("Brief Intro", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
|
||||
// Test setting an empty summary (should default to DEFAULT)
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
test_summarize_error(&model, &thread, cx);
|
||||
|
||||
// Now we should be able to set a summary
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summary("Brief Intro", cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(cx, json!({})).await;
|
||||
|
||||
let (_, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
test_summarize_error(&model, &thread, cx);
|
||||
|
||||
// Sending another message should not trigger another summarize request
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message(
|
||||
"How are you?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
// State is still Error, not Generating
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Error));
|
||||
});
|
||||
|
||||
// But the summarize request can be invoked manually
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.summarize(cx);
|
||||
});
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Generating));
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("A successful summary".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Ready(_)));
|
||||
assert_eq!(thread.summary().or_default(), "A successful summary");
|
||||
});
|
||||
}
|
||||
|
||||
fn test_summarize_error(
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
thread: &Entity<Thread>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx);
|
||||
thread.send_to_model(model.clone(), None, cx);
|
||||
});
|
||||
|
||||
let fake_model = model.as_fake();
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Generating));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
|
||||
// Simulate summary request ending
|
||||
cx.run_until_parked();
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// State is set to Error and default message
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert!(matches!(thread.summary(), ThreadSummary::Error));
|
||||
assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Assistant response".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
@@ -3303,9 +3509,29 @@ fn main() {{
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
let model = FakeLanguageModel::default();
|
||||
let provider = Arc::new(FakeLanguageModelProvider);
|
||||
let model = provider.test_model();
|
||||
let model: Arc<dyn LanguageModel> = Arc::new(model);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: provider.clone(),
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
registry.set_thread_summary_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
(workspace, thread_store, thread, context_store, model)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,11 @@ use std::fmt::Display;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
@@ -18,7 +17,6 @@ use ui::{
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
use crate::{AgentPanel, RemoveSelectedThread};
|
||||
|
||||
pub struct ThreadHistory {
|
||||
@@ -26,11 +24,12 @@ pub struct ThreadHistory {
|
||||
history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
all_entries: Arc<Vec<HistoryEntry>>,
|
||||
// When the search is empty, we display date separators between history entries
|
||||
// This vector contains an enum of either a separator or an actual entry
|
||||
separated_items: Vec<HistoryListItem>,
|
||||
separated_items: Vec<ListItemType>,
|
||||
// Maps entry indexes to list item indexes
|
||||
separated_item_indexes: Vec<u32>,
|
||||
_separated_items_task: Option<Task<()>>,
|
||||
@@ -52,7 +51,7 @@ enum SearchState {
|
||||
},
|
||||
}
|
||||
|
||||
enum HistoryListItem {
|
||||
enum ListItemType {
|
||||
BucketSeparator(TimeBucket),
|
||||
Entry {
|
||||
index: usize,
|
||||
@@ -60,11 +59,11 @@ enum HistoryListItem {
|
||||
},
|
||||
}
|
||||
|
||||
impl HistoryListItem {
|
||||
impl ListItemType {
|
||||
fn entry_index(&self) -> Option<usize> {
|
||||
match self {
|
||||
HistoryListItem::BucketSeparator(_) => None,
|
||||
HistoryListItem::Entry { index, .. } => Some(*index),
|
||||
ListItemType::BucketSeparator(_) => None,
|
||||
ListItemType::Entry { index, .. } => Some(*index),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,6 +101,7 @@ impl ThreadHistory {
|
||||
history_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
search_state: SearchState::Empty,
|
||||
all_entries: Default::default(),
|
||||
separated_items: Default::default(),
|
||||
@@ -117,40 +117,21 @@ impl ThreadHistory {
|
||||
}
|
||||
|
||||
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.all_entries = self
|
||||
let new_entries: Arc<Vec<HistoryEntry>> = self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.entries(cx))
|
||||
.into();
|
||||
|
||||
self.set_selected_entry_index(0, cx);
|
||||
self.update_separated_items(cx);
|
||||
|
||||
match &self.search_state {
|
||||
SearchState::Empty => {}
|
||||
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
|
||||
self.search(query.clone(), cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_separated_items(&mut self, cx: &mut Context<Self>) {
|
||||
self._separated_items_task.take();
|
||||
let all_entries = self.all_entries.clone();
|
||||
|
||||
let mut items = std::mem::take(&mut self.separated_items);
|
||||
let mut indexes = std::mem::take(&mut self.separated_item_indexes);
|
||||
items.clear();
|
||||
indexes.clear();
|
||||
// We know there's going to be at least one bucket separator
|
||||
items.reserve(all_entries.len() + 1);
|
||||
indexes.reserve(all_entries.len() + 1);
|
||||
let mut items = Vec::with_capacity(new_entries.len() + 1);
|
||||
let mut indexes = Vec::with_capacity(new_entries.len() + 1);
|
||||
|
||||
let bg_task = cx.background_spawn(async move {
|
||||
let mut bucket = None;
|
||||
let today = Local::now().naive_local().date();
|
||||
|
||||
for (index, entry) in all_entries.iter().enumerate() {
|
||||
for (index, entry) in new_entries.iter().enumerate() {
|
||||
let entry_date = entry
|
||||
.updated_at()
|
||||
.with_timezone(&Local)
|
||||
@@ -160,23 +141,50 @@ impl ThreadHistory {
|
||||
|
||||
if Some(entry_bucket) != bucket {
|
||||
bucket = Some(entry_bucket);
|
||||
items.push(HistoryListItem::BucketSeparator(entry_bucket));
|
||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||
}
|
||||
|
||||
indexes.push(items.len() as u32);
|
||||
items.push(HistoryListItem::Entry {
|
||||
items.push(ListItemType::Entry {
|
||||
index,
|
||||
format: entry_bucket.into(),
|
||||
});
|
||||
}
|
||||
(items, indexes)
|
||||
(new_entries, items, indexes)
|
||||
});
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let (items, indexes) = bg_task.await;
|
||||
let (new_entries, items, indexes) = bg_task.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let previously_selected_entry =
|
||||
this.all_entries.get(this.selected_index).map(|e| e.id());
|
||||
|
||||
this.all_entries = new_entries;
|
||||
this.separated_items = items;
|
||||
this.separated_item_indexes = indexes;
|
||||
|
||||
match &this.search_state {
|
||||
SearchState::Empty => {
|
||||
if this.selected_index >= this.all_entries.len() {
|
||||
this.set_selected_entry_index(
|
||||
this.all_entries.len().saturating_sub(1),
|
||||
cx,
|
||||
);
|
||||
} else if let Some(prev_id) = previously_selected_entry {
|
||||
if let Some(new_ix) = this
|
||||
.all_entries
|
||||
.iter()
|
||||
.position(|probe| probe.id() == prev_id)
|
||||
{
|
||||
this.set_selected_entry_index(new_ix, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
|
||||
this.search(query.clone(), cx);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
@@ -467,7 +475,7 @@ impl ThreadHistory {
|
||||
.map(|(ix, m)| {
|
||||
self.render_list_item(
|
||||
Some(range_start + ix),
|
||||
&HistoryListItem::Entry {
|
||||
&ListItemType::Entry {
|
||||
index: m.candidate_id,
|
||||
format: EntryTimeFormat::DateAndTime,
|
||||
},
|
||||
@@ -485,25 +493,36 @@ impl ThreadHistory {
|
||||
fn render_list_item(
|
||||
&self,
|
||||
list_entry_ix: Option<usize>,
|
||||
item: &HistoryListItem,
|
||||
item: &ListItemType,
|
||||
highlight_positions: Vec<usize>,
|
||||
cx: &App,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
match item {
|
||||
HistoryListItem::Entry { index, format } => match self.all_entries.get(*index) {
|
||||
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
|
||||
Some(entry) => h_flex()
|
||||
.w_full()
|
||||
.pb_1()
|
||||
.child(self.render_history_entry(
|
||||
entry,
|
||||
list_entry_ix == Some(self.selected_index),
|
||||
highlight_positions,
|
||||
*format,
|
||||
))
|
||||
.child(
|
||||
HistoryEntryElement::new(entry.clone(), self.agent_panel.clone())
|
||||
.highlight_positions(highlight_positions)
|
||||
.timestamp_format(*format)
|
||||
.selected(list_entry_ix == Some(self.selected_index))
|
||||
.hovered(list_entry_ix == self.hovered_index)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = list_entry_ix;
|
||||
} else if this.hovered_index == list_entry_ix {
|
||||
this.hovered_index = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any(),
|
||||
None => Empty.into_any_element(),
|
||||
},
|
||||
HistoryListItem::BucketSeparator(bucket) => div()
|
||||
ListItemType::BucketSeparator(bucket) => div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
@@ -515,33 +534,6 @@ impl ThreadHistory {
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_history_entry(
|
||||
&self,
|
||||
entry: &HistoryEntry,
|
||||
is_active: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
format: EntryTimeFormat,
|
||||
) -> AnyElement {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
self.agent_panel.clone(),
|
||||
is_active,
|
||||
highlight_positions,
|
||||
format,
|
||||
)
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
self.agent_panel.clone(),
|
||||
is_active,
|
||||
highlight_positions,
|
||||
format,
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ThreadHistory {
|
||||
@@ -623,155 +615,97 @@ impl Render for ThreadHistory {
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: SerializedThreadMetadata,
|
||||
pub struct HistoryEntryElement {
|
||||
entry: HistoryEntry,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
hovered: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(
|
||||
thread: SerializedThreadMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
) -> Self {
|
||||
impl HistoryEntryElement {
|
||||
pub fn new(entry: HistoryEntry, agent_panel: WeakEntity<AgentPanel>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
entry,
|
||||
agent_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
timestamp_format,
|
||||
selected: false,
|
||||
hovered: false,
|
||||
highlight_positions: vec![],
|
||||
timestamp_format: EntryTimeFormat::DateAndTime,
|
||||
on_hover: Box::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hovered(mut self, hovered: bool) -> Self {
|
||||
self.hovered = hovered;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
|
||||
self.highlight_positions = positions;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_hover = Box::new(on_hover);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self {
|
||||
self.timestamp_format = format;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
impl RenderOnce for HistoryEntryElement {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.thread.summary;
|
||||
let (id, summary, timestamp) = match &self.entry {
|
||||
HistoryEntry::Thread(thread) => (
|
||||
thread.id.to_string(),
|
||||
thread.summary.clone(),
|
||||
thread.updated_at.timestamp(),
|
||||
),
|
||||
HistoryEntry::Context(context) => (
|
||||
context.path.to_string_lossy().to_string(),
|
||||
context.title.clone().into(),
|
||||
context.mtime.timestamp(),
|
||||
),
|
||||
};
|
||||
|
||||
let thread_timestamp = self.timestamp_format.format_timestamp(
|
||||
&self.agent_panel,
|
||||
self.thread.updated_at.timestamp(),
|
||||
cx,
|
||||
);
|
||||
let thread_timestamp =
|
||||
self.timestamp_format
|
||||
.format_timestamp(&self.agent_panel, timestamp, cx);
|
||||
|
||||
ListItem::new(SharedString::from(self.thread.id.to_string()))
|
||||
ListItem::new(SharedString::from(id))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div().max_w_4_5().child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx).detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastContext {
|
||||
context: SavedContextMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
}
|
||||
|
||||
impl PastContext {
|
||||
pub fn new(
|
||||
context: SavedContextMetadata,
|
||||
agent_panel: WeakEntity<AgentPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
timestamp_format: EntryTimeFormat,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
agent_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
timestamp_format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.context.mtime.timestamp(),
|
||||
cx,
|
||||
);
|
||||
|
||||
ListItem::new(SharedString::from(
|
||||
self.context.path.to_string_lossy().to_string(),
|
||||
))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div().max_w_4_5().child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new(context_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
.on_hover(self.on_hover)
|
||||
.end_slot::<IconButton>(if self.hovered || self.selected {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -781,30 +715,70 @@ impl RenderOnce for PastContext {
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> =
|
||||
match &self.entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
let id = thread.id.clone();
|
||||
|
||||
Box::new(move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
let path = context.path.clone();
|
||||
|
||||
Box::new(move |_event, _window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
};
|
||||
f
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click({
|
||||
let agent_panel = self.agent_panel.clone();
|
||||
|
||||
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> = match &self.entry
|
||||
{
|
||||
HistoryEntry::Thread(thread) => {
|
||||
let id = thread.id.clone();
|
||||
Box::new(move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_by_id(&id, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
let path = context.path.clone();
|
||||
Box::new(move |_event, window, cx| {
|
||||
agent_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
};
|
||||
f
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -486,8 +486,8 @@ impl ThreadStore {
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.into_iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
);
|
||||
@@ -511,32 +511,32 @@ impl ThreadStore {
|
||||
});
|
||||
}
|
||||
// Enable all the tools from all context servers, but disable the ones that are explicitly disabled
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
for (context_server_id, preset) in profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.disable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
id: context_server_id.into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| (!enabled).then(|| tool.clone()))
|
||||
.into_iter()
|
||||
.filter_map(|(tool, enabled)| (!enabled).then(|| tool))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
for (context_server_id, preset) in profile.context_servers {
|
||||
self.tools.update(cx, |tools, cx| {
|
||||
tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
id: context_server_id.into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.into_iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool))
|
||||
.collect::<Vec<_>>(),
|
||||
cx,
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl RenderOnce for UsageCallout {
|
||||
|
||||
let (title, message, button_text, url) = if is_limit_reached {
|
||||
match self.plan {
|
||||
Plan::Free => (
|
||||
Plan::ZedFree => (
|
||||
"Out of free prompts",
|
||||
"Upgrade to continue, wait for the next reset, or switch to API key."
|
||||
.to_string(),
|
||||
@@ -61,7 +61,7 @@ impl RenderOnce for UsageCallout {
|
||||
}
|
||||
} else {
|
||||
match self.plan {
|
||||
Plan::Free => (
|
||||
Plan::ZedFree => (
|
||||
"Reaching free plan limit soon",
|
||||
format!(
|
||||
"{remaining} remaining - Upgrade to increase limit, or switch providers",
|
||||
@@ -120,7 +120,7 @@ impl Component for UsageCallout {
|
||||
single_example(
|
||||
"Approaching limit (90%)",
|
||||
UsageCallout::new(
|
||||
Plan::Free,
|
||||
Plan::ZedFree,
|
||||
RequestUsage {
|
||||
limit: UsageLimit::Limited(50),
|
||||
amount: 45, // 90% of limit
|
||||
@@ -131,7 +131,7 @@ impl Component for UsageCallout {
|
||||
single_example(
|
||||
"Limit reached (100%)",
|
||||
UsageCallout::new(
|
||||
Plan::Free,
|
||||
Plan::ZedFree,
|
||||
RequestUsage {
|
||||
limit: UsageLimit::Limited(50),
|
||||
amount: 50, // 100% of limit
|
||||
|
||||
@@ -167,16 +167,20 @@ fn get_shell_safe_zed_path() -> anyhow::Result<String> {
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// sanity check on unix systems that the path exists and is executable
|
||||
// todo(windows): implement this check for windows (or just use `is-executable` crate)
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
let metadata = std::fs::metadata(&zed_path)
|
||||
.context("Failed to check metadata of Zed executable path for use in askpass")?;
|
||||
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
|
||||
anyhow::ensure!(
|
||||
is_executable,
|
||||
"Failed to verify Zed executable path for use in askpass"
|
||||
);
|
||||
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
|
||||
// (see https://github.com/zed-industries/zed/issues/29819)
|
||||
// The zed path failing to execute within the askpass script results in very vague ssh
|
||||
// authentication failed errors, so this was done to try and surface a better error
|
||||
//
|
||||
// use std::os::unix::fs::MetadataExt;
|
||||
// let metadata = std::fs::metadata(&zed_path)
|
||||
// .context("Failed to check metadata of Zed executable path for use in askpass")?;
|
||||
// let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
|
||||
// anyhow::ensure!(
|
||||
// is_executable,
|
||||
// "Failed to verify Zed executable path for use in askpass"
|
||||
// );
|
||||
|
||||
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
|
||||
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
|
||||
// errors are introduced in the future :(
|
||||
|
||||
@@ -8,7 +8,8 @@ mod slash_command_picker;
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::Client;
|
||||
use gpui::App;
|
||||
use gpui::{App, Context};
|
||||
use workspace::Workspace;
|
||||
|
||||
pub use crate::context::*;
|
||||
pub use crate::context_editor::*;
|
||||
@@ -16,6 +17,18 @@ pub use crate::context_history::*;
|
||||
pub use crate::context_store::*;
|
||||
pub use crate::slash_command::*;
|
||||
|
||||
pub fn init(client: Arc<Client>, _cx: &mut App) {
|
||||
pub fn init(client: Arc<Client>, cx: &mut App) {
|
||||
context_store::init(&client.into());
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
|
||||
cx.observe_new(
|
||||
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
||||
workspace
|
||||
.register_action(ContextEditor::quote_selection)
|
||||
.register_action(ContextEditor::insert_selection)
|
||||
.register_action(ContextEditor::copy_code)
|
||||
.register_action(ContextEditor::handle_insert_dragged_files);
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#[cfg(test)]
|
||||
mod context_tests;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
|
||||
@@ -133,7 +133,7 @@ pub enum ContextOperation {
|
||||
version: clock::Global,
|
||||
},
|
||||
UpdateSummary {
|
||||
summary: ContextSummary,
|
||||
summary: ContextSummaryContent,
|
||||
version: clock::Global,
|
||||
},
|
||||
SlashCommandStarted {
|
||||
@@ -203,7 +203,7 @@ impl ContextOperation {
|
||||
version: language::proto::deserialize_version(&update.version),
|
||||
}),
|
||||
proto::context_operation::Variant::UpdateSummary(update) => Ok(Self::UpdateSummary {
|
||||
summary: ContextSummary {
|
||||
summary: ContextSummaryContent {
|
||||
text: update.summary,
|
||||
done: update.done,
|
||||
timestamp: language::proto::deserialize_timestamp(
|
||||
@@ -467,11 +467,73 @@ pub enum ContextEvent {
|
||||
Operation(ContextOperation),
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
pub struct ContextSummary {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ContextSummary {
|
||||
Pending,
|
||||
Content(ContextSummaryContent),
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ContextSummaryContent {
|
||||
pub text: String,
|
||||
pub done: bool,
|
||||
timestamp: clock::Lamport,
|
||||
pub timestamp: clock::Lamport,
|
||||
}
|
||||
|
||||
impl ContextSummary {
|
||||
pub const DEFAULT: &str = "New Text Thread";
|
||||
|
||||
pub fn or_default(&self) -> SharedString {
|
||||
self.unwrap_or(Self::DEFAULT)
|
||||
}
|
||||
|
||||
pub fn unwrap_or(&self, message: impl Into<SharedString>) -> SharedString {
|
||||
self.content()
|
||||
.map_or_else(|| message.into(), |content| content.text.clone().into())
|
||||
}
|
||||
|
||||
pub fn content(&self) -> Option<&ContextSummaryContent> {
|
||||
match self {
|
||||
ContextSummary::Content(content) => Some(content),
|
||||
ContextSummary::Pending | ContextSummary::Error => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_as_mut(&mut self) -> Option<&mut ContextSummaryContent> {
|
||||
match self {
|
||||
ContextSummary::Content(content) => Some(content),
|
||||
ContextSummary::Pending | ContextSummary::Error => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content_or_set_empty(&mut self) -> &mut ContextSummaryContent {
|
||||
match self {
|
||||
ContextSummary::Content(content) => content,
|
||||
ContextSummary::Pending | ContextSummary::Error => {
|
||||
let content = ContextSummaryContent::default();
|
||||
*self = ContextSummary::Content(content);
|
||||
self.content_as_mut().unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self, ContextSummary::Pending)
|
||||
}
|
||||
|
||||
fn timestamp(&self) -> Option<clock::Lamport> {
|
||||
match self {
|
||||
ContextSummary::Content(content) => Some(content.timestamp),
|
||||
ContextSummary::Pending | ContextSummary::Error => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ContextSummary {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.timestamp().partial_cmp(&other.timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -607,7 +669,7 @@ pub struct AssistantContext {
|
||||
message_anchors: Vec<MessageAnchor>,
|
||||
contents: Vec<Content>,
|
||||
messages_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: Option<ContextSummary>,
|
||||
summary: ContextSummary,
|
||||
summary_task: Task<Option<()>>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
@@ -694,7 +756,7 @@ impl AssistantContext {
|
||||
slash_command_output_sections: Vec::new(),
|
||||
thought_process_output_sections: Vec::new(),
|
||||
edits_since_last_parse: edits_since_last_slash_command_parse,
|
||||
summary: None,
|
||||
summary: ContextSummary::Pending,
|
||||
summary_task: Task::ready(None),
|
||||
completion_count: Default::default(),
|
||||
pending_completions: Default::default(),
|
||||
@@ -753,7 +815,7 @@ impl AssistantContext {
|
||||
.collect(),
|
||||
summary: self
|
||||
.summary
|
||||
.as_ref()
|
||||
.content()
|
||||
.map(|summary| summary.text.clone())
|
||||
.unwrap_or_default(),
|
||||
slash_command_output_sections: self
|
||||
@@ -939,12 +1001,10 @@ impl AssistantContext {
|
||||
summary: new_summary,
|
||||
..
|
||||
} => {
|
||||
if self
|
||||
.summary
|
||||
.as_ref()
|
||||
.map_or(true, |summary| new_summary.timestamp > summary.timestamp)
|
||||
{
|
||||
self.summary = Some(new_summary);
|
||||
if self.summary.timestamp().map_or(true, |current_timestamp| {
|
||||
new_summary.timestamp > current_timestamp
|
||||
}) {
|
||||
self.summary = ContextSummary::Content(new_summary);
|
||||
summary_generated = true;
|
||||
}
|
||||
}
|
||||
@@ -1102,8 +1162,8 @@ impl AssistantContext {
|
||||
self.path.as_ref()
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<&ContextSummary> {
|
||||
self.summary.as_ref()
|
||||
pub fn summary(&self) -> &ContextSummary {
|
||||
&self.summary
|
||||
}
|
||||
|
||||
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
|
||||
@@ -2576,7 +2636,7 @@ impl AssistantContext {
|
||||
return;
|
||||
};
|
||||
|
||||
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) {
|
||||
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_pending()) {
|
||||
if !model.provider.is_authenticated(cx) {
|
||||
return;
|
||||
}
|
||||
@@ -2593,17 +2653,20 @@ impl AssistantContext {
|
||||
|
||||
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
|
||||
// be displayed.
|
||||
if self.summary.is_none() {
|
||||
self.summary = Some(ContextSummary {
|
||||
text: "".to_string(),
|
||||
done: false,
|
||||
timestamp: clock::Lamport::default(),
|
||||
});
|
||||
replace_old = true;
|
||||
match self.summary {
|
||||
ContextSummary::Pending | ContextSummary::Error => {
|
||||
self.summary = ContextSummary::Content(ContextSummaryContent {
|
||||
text: "".to_string(),
|
||||
done: false,
|
||||
timestamp: clock::Lamport::default(),
|
||||
});
|
||||
replace_old = true;
|
||||
}
|
||||
ContextSummary::Content(_) => {}
|
||||
}
|
||||
|
||||
self.summary_task = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let result = async {
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
|
||||
@@ -2614,7 +2677,7 @@ impl AssistantContext {
|
||||
this.update(cx, |this, cx| {
|
||||
let version = this.version.clone();
|
||||
let timestamp = this.next_timestamp();
|
||||
let summary = this.summary.get_or_insert(ContextSummary::default());
|
||||
let summary = this.summary.content_or_set_empty();
|
||||
if !replaced && replace_old {
|
||||
summary.text.clear();
|
||||
replaced = true;
|
||||
@@ -2636,10 +2699,19 @@ impl AssistantContext {
|
||||
}
|
||||
}
|
||||
|
||||
this.read_with(cx, |this, _cx| {
|
||||
if let Some(summary) = this.summary.content() {
|
||||
if summary.text.is_empty() {
|
||||
bail!("Model generated an empty summary");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})??;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let version = this.version.clone();
|
||||
let timestamp = this.next_timestamp();
|
||||
if let Some(summary) = this.summary.as_mut() {
|
||||
if let Some(summary) = this.summary.content_as_mut() {
|
||||
summary.done = true;
|
||||
summary.timestamp = timestamp;
|
||||
let operation = ContextOperation::UpdateSummary {
|
||||
@@ -2654,8 +2726,18 @@ impl AssistantContext {
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
.await;
|
||||
|
||||
if let Err(err) = result {
|
||||
this.update(cx, |this, cx| {
|
||||
this.summary = ContextSummary::Error;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
})
|
||||
.log_err();
|
||||
log::error!("Error generating context summary: {}", err);
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2769,7 +2851,7 @@ impl AssistantContext {
|
||||
|
||||
let (old_path, summary) = this.read_with(cx, |this, _| {
|
||||
let path = this.path.clone();
|
||||
let summary = if let Some(summary) = this.summary.as_ref() {
|
||||
let summary = if let Some(summary) = this.summary.content() {
|
||||
if summary.done {
|
||||
Some(summary.text.clone())
|
||||
} else {
|
||||
@@ -2823,21 +2905,12 @@ impl AssistantContext {
|
||||
|
||||
pub fn set_custom_summary(&mut self, custom_summary: String, cx: &mut Context<Self>) {
|
||||
let timestamp = self.next_timestamp();
|
||||
let summary = self.summary.get_or_insert(ContextSummary::default());
|
||||
let summary = self.summary.content_or_set_empty();
|
||||
summary.timestamp = timestamp;
|
||||
summary.done = true;
|
||||
summary.text = custom_summary;
|
||||
cx.emit(ContextEvent::SummaryChanged);
|
||||
}
|
||||
|
||||
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Text Thread");
|
||||
|
||||
pub fn summary_or_default(&self) -> SharedString {
|
||||
self.summary
|
||||
.as_ref()
|
||||
.map(|summary| summary.text.clone().into())
|
||||
.unwrap_or(Self::DEFAULT_SUMMARY)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -3053,7 +3126,7 @@ impl SavedContext {
|
||||
|
||||
let timestamp = next_timestamp.tick();
|
||||
operations.push(ContextOperation::UpdateSummary {
|
||||
summary: ContextSummary {
|
||||
summary: ContextSummaryContent {
|
||||
text: self.summary,
|
||||
done: true,
|
||||
timestamp,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation,
|
||||
AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation, ContextSummary,
|
||||
InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -16,7 +16,10 @@ use futures::{
|
||||
};
|
||||
use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelCacheConfiguration, LanguageModelRegistry, Role,
|
||||
fake_provider::{FakeLanguageModel, FakeLanguageModelProvider},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::Project;
|
||||
@@ -1177,6 +1180,187 @@ fn test_mark_cache_anchors(cx: &mut App) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_summarization(cx: &mut TestAppContext) {
|
||||
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
|
||||
|
||||
// Initial state should be pending
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(matches!(context.summary(), ContextSummary::Pending));
|
||||
assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
|
||||
});
|
||||
|
||||
let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
|
||||
context.update(cx, |context, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Send a message
|
||||
context.update(cx, |context, cx| {
|
||||
context.assist(cx);
|
||||
});
|
||||
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
// Should start generating summary when there are >= 2 messages
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(!context.summary().content().unwrap().done);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Brief".into());
|
||||
fake_model.stream_last_completion_response(" Introduction".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Summary should be set
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "Brief Introduction");
|
||||
});
|
||||
|
||||
// We should be able to manually set a summary
|
||||
context.update(cx, |context, cx| {
|
||||
context.set_custom_summary("Brief Intro".into(), cx);
|
||||
});
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) {
|
||||
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
|
||||
|
||||
test_summarize_error(&fake_model, &context, cx);
|
||||
|
||||
// Now we should be able to set a summary
|
||||
context.update(cx, |context, cx| {
|
||||
context.set_custom_summary("Brief Intro".into(), cx);
|
||||
});
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "Brief Intro");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_summary_error_retry(cx: &mut TestAppContext) {
|
||||
let (context, fake_model) = setup_context_editor_with_fake_model(cx);
|
||||
|
||||
test_summarize_error(&fake_model, &context, cx);
|
||||
|
||||
// Sending another message should not trigger another summarize request
|
||||
context.update(cx, |context, cx| {
|
||||
context.assist(cx);
|
||||
});
|
||||
|
||||
simulate_successful_response(&fake_model, cx);
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
// State is still Error, not Generating
|
||||
assert!(matches!(context.summary(), ContextSummary::Error));
|
||||
});
|
||||
|
||||
// But the summarize request can be invoked manually
|
||||
context.update(cx, |context, cx| {
|
||||
context.summarize(true, cx);
|
||||
});
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(!context.summary().content().unwrap().done);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("A successful summary".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(context.summary().or_default(), "A successful summary");
|
||||
});
|
||||
}
|
||||
|
||||
fn test_summarize_error(
|
||||
model: &Arc<FakeLanguageModel>,
|
||||
context: &Entity<AssistantContext>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone());
|
||||
context.update(cx, |context, cx| {
|
||||
context
|
||||
.insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// Send a message
|
||||
context.update(cx, |context, cx| {
|
||||
context.assist(cx);
|
||||
});
|
||||
|
||||
simulate_successful_response(&model, cx);
|
||||
|
||||
context.read_with(cx, |context, _| {
|
||||
assert!(!context.summary().content().unwrap().done);
|
||||
});
|
||||
|
||||
// Simulate summary request ending
|
||||
cx.run_until_parked();
|
||||
model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
|
||||
// State is set to Error and default message
|
||||
context.read_with(cx, |context, _| {
|
||||
assert_eq!(*context.summary(), ContextSummary::Error);
|
||||
assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT);
|
||||
});
|
||||
}
|
||||
|
||||
fn setup_context_editor_with_fake_model(
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<AssistantContext>, Arc<FakeLanguageModel>) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor().clone()));
|
||||
|
||||
let fake_provider = Arc::new(FakeLanguageModelProvider);
|
||||
let fake_model = Arc::new(fake_provider.test_model());
|
||||
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: fake_provider.clone(),
|
||||
model: fake_model.clone(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
AssistantContext::local(
|
||||
registry,
|
||||
None,
|
||||
None,
|
||||
prompt_builder.clone(),
|
||||
Arc::new(SlashCommandWorkingSet::default()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
(context, fake_model)
|
||||
}
|
||||
|
||||
fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) {
|
||||
cx.run_until_parked();
|
||||
fake_model.stream_last_completion_response("Assistant response".into());
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn messages(context: &Entity<AssistantContext>, cx: &App) -> Vec<(MessageId, Role, Range<usize>)> {
|
||||
context
|
||||
.read(cx)
|
||||
|
||||
@@ -1860,7 +1860,12 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.context.read(cx).summary_or_default()
|
||||
self.context.read(cx).summary().or_default()
|
||||
}
|
||||
|
||||
pub fn regenerate_summary(&mut self, cx: &mut Context<Self>) {
|
||||
self.context
|
||||
.update(cx, |context, cx| context.summarize(true, cx));
|
||||
}
|
||||
|
||||
fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
@@ -1894,11 +1899,24 @@ impl ContextEditor {
|
||||
.log_err();
|
||||
|
||||
if let Some(client) = client {
|
||||
cx.spawn(async move |this, cx| {
|
||||
client.authenticate_and_connect(true, cx).await?;
|
||||
this.update(cx, |_, cx| cx.notify())
|
||||
cx.spawn(async move |context_editor, cx| {
|
||||
match client.authenticate_and_connect(true, cx).await {
|
||||
util::ConnectionResult::Timeout => {
|
||||
log::error!("Authentication timeout")
|
||||
}
|
||||
util::ConnectionResult::ConnectionReset => {
|
||||
log::error!("Connection reset")
|
||||
}
|
||||
util::ConnectionResult::Result(r) => {
|
||||
if r.log_err().is_some() {
|
||||
context_editor
|
||||
.update(cx, |_, cx| cx.notify())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
.detach()
|
||||
}
|
||||
})),
|
||||
)
|
||||
|
||||
@@ -648,7 +648,10 @@ impl ContextStore {
|
||||
if context.replica_id() == ReplicaId::default() {
|
||||
Some(proto::ContextMetadata {
|
||||
context_id: context.id().to_proto(),
|
||||
summary: context.summary().map(|summary| summary.text.clone()),
|
||||
summary: context
|
||||
.summary()
|
||||
.content()
|
||||
.map(|summary| summary.text.clone()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -278,8 +278,8 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
|
||||
let arguments = call
|
||||
.arguments
|
||||
.iter()
|
||||
.filter_map(|argument| Some(line.get(argument.clone())?.to_string()))
|
||||
.into_iter()
|
||||
.filter_map(|argument| Some(line.get(argument)?.to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let argument_range = first_arg_start..buffer_position;
|
||||
(
|
||||
|
||||
@@ -19,7 +19,6 @@ gpui.workspace = true
|
||||
indexmap.workspace = true
|
||||
language_model.workspace = true
|
||||
lmstudio = { workspace = true, features = ["schemars"] }
|
||||
log.workspace = true
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
|
||||
@@ -84,7 +84,6 @@ pub struct AssistantSettings {
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
@@ -150,358 +149,52 @@ impl LanguageModelParameters {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
|
||||
pub struct AssistantSettingsContent {
|
||||
#[serde(flatten)]
|
||||
pub inner: Option<AssistantSettingsContentInner>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContentInner {
|
||||
Versioned(Box<VersionedAssistantSettingsContent>),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
impl AssistantSettingsContentInner {
|
||||
fn for_v2(content: AssistantSettingsContentV2) -> Self {
|
||||
AssistantSettingsContentInner::Versioned(Box::new(VersionedAssistantSettingsContent::V2(
|
||||
content,
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for AssistantSettingsContent {
|
||||
fn schema_name() -> String {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(r#gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
VersionedAssistantSettingsContent::is_referenceable()
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match &self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(_)) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match &self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_width,
|
||||
default_model: settings
|
||||
.provider
|
||||
.clone()
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::LmStudio { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "lmstudio".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "deepseek".into(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
None => AssistantSettingsContentV2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match &mut self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
dock: Some(dock),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
self.dock = Some(dock);
|
||||
}
|
||||
|
||||
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
|
||||
let model = language_model.id().0.to_string();
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match &mut self.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(settings)) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(
|
||||
&model,
|
||||
None,
|
||||
None,
|
||||
Some(language_model.supports_tools()),
|
||||
)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
|
||||
settings.default_open_ai_model = Some(model);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
self.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn v2_setting(
|
||||
&mut self,
|
||||
f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
|
||||
) -> anyhow::Result<()> {
|
||||
match self.inner.get_or_insert_with(|| {
|
||||
AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
|
||||
..Default::default()
|
||||
})
|
||||
}) {
|
||||
AssistantSettingsContentInner::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
f(settings)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
self.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.always_allow_tool_actions = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.always_allow_tool_actions = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_single_file_review(&mut self, allow: bool) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.single_file_review = Some(allow);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.single_file_review = Some(allow);
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.default_profile = Some(profile_id);
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
self.default_profile = Some(profile_id);
|
||||
}
|
||||
|
||||
pub fn create_profile(
|
||||
@@ -509,74 +202,38 @@ impl AssistantSettingsContent {
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
) -> Result<()> {
|
||||
self.v2_setting(|settings| {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
let profiles = self.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
enable_all_context_servers: Some(profile.enable_all_context_servers),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
enable_all_context_servers: Some(profile.enable_all_context_servers),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[serde(tag = "version")]
|
||||
pub enum VersionedAssistantSettingsContent {
|
||||
#[serde(rename = "1")]
|
||||
V1(AssistantSettingsContentV1),
|
||||
#[serde(rename = "2")]
|
||||
V2(AssistantSettingsContentV2),
|
||||
}
|
||||
|
||||
impl Default for VersionedAssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::V2(AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
pub struct AssistantSettingsContentV2 {
|
||||
pub struct AssistantSettingsContent {
|
||||
/// Whether the Assistant is enabled.
|
||||
///
|
||||
/// Default: true
|
||||
@@ -806,11 +463,6 @@ impl Settings for AssistantSettings {
|
||||
let mut settings = AssistantSettings::default();
|
||||
|
||||
for value in sources.defaults_and_customizations() {
|
||||
if value.is_version_outdated() {
|
||||
settings.using_outdated_settings_version = true;
|
||||
}
|
||||
|
||||
let value = value.upgrade();
|
||||
merge(&mut settings.enabled, value.enabled);
|
||||
merge(&mut settings.button, value.button);
|
||||
merge(&mut settings.dock, value.dock);
|
||||
@@ -822,17 +474,23 @@ impl Settings for AssistantSettings {
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.default_model, value.default_model.clone());
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.clone()
|
||||
.or(settings.inline_assistant_model.take());
|
||||
settings.commit_message_model = value
|
||||
.commit_message_model
|
||||
.clone()
|
||||
.or(settings.commit_message_model.take());
|
||||
settings.thread_summary_model = value
|
||||
.thread_summary_model
|
||||
.clone()
|
||||
.or(settings.thread_summary_model.take());
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.inline_alternatives,
|
||||
value.inline_alternatives.clone(),
|
||||
);
|
||||
merge(
|
||||
&mut settings.always_allow_tool_actions,
|
||||
value.always_allow_tool_actions,
|
||||
@@ -843,7 +501,7 @@ impl Settings for AssistantSettings {
|
||||
);
|
||||
merge(&mut settings.stream_edits, value.stream_edits);
|
||||
merge(&mut settings.single_file_review, value.single_file_review);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
merge(&mut settings.default_profile, value.default_profile.clone());
|
||||
merge(
|
||||
&mut settings.preferred_completion_mode,
|
||||
value.preferred_completion_mode,
|
||||
@@ -853,7 +511,7 @@ impl Settings for AssistantSettings {
|
||||
.model_parameters
|
||||
.extend_from_slice(&value.model_parameters);
|
||||
|
||||
if let Some(profiles) = value.profiles {
|
||||
if let Some(profiles) = value.profiles.clone() {
|
||||
settings
|
||||
.profiles
|
||||
.extend(profiles.into_iter().map(|(id, profile)| {
|
||||
@@ -891,31 +549,8 @@ impl Settings for AssistantSettings {
|
||||
.read_value("chat.agent.enabled")
|
||||
.and_then(|b| b.as_bool())
|
||||
{
|
||||
match &mut current.inner {
|
||||
Some(AssistantSettingsContentInner::Versioned(versioned)) => {
|
||||
match versioned.as_mut() {
|
||||
VersionedAssistantSettingsContent::V1(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
|
||||
VersionedAssistantSettingsContent::V2(setting) => {
|
||||
setting.enabled = Some(b);
|
||||
setting.button = Some(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
|
||||
None => {
|
||||
current.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(b),
|
||||
button: Some(b),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
current.enabled = Some(b);
|
||||
current.button = Some(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -925,150 +560,3 @@ fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
*target = value;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fs::Fs;
|
||||
use gpui::{ReadGlobal, TestAppContext};
|
||||
use settings::SettingsStore;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let test_settings = settings::SettingsStore::test(cx);
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
|
||||
assert_eq!(
|
||||
AssistantSettings::get_global(cx).default_model,
|
||||
LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
model: "claude-3-7-sonnet-latest".into(),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent {
|
||||
inner: Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
dock: None,
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
)),
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
|
||||
assert!(raw_settings_value.contains(r#""version": "2""#));
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
agent: AssistantSettingsContent,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest =
|
||||
serde_json_lenient::from_str(&raw_settings_value).unwrap();
|
||||
|
||||
assert!(!assistant_settings.agent.is_version_outdated());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_load_settings_from_old_key(cx: &mut TestAppContext) {
|
||||
let fs = fs::FakeFs::new(cx.executor().clone());
|
||||
fs.create_dir(paths::settings_file().parent().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let mut test_settings = settings::SettingsStore::test(cx);
|
||||
let user_settings_content = r#"{
|
||||
"assistant": {
|
||||
"enabled": true,
|
||||
"version": "2",
|
||||
"default_model": {
|
||||
"provider": "zed.dev",
|
||||
"model": "gpt-99"
|
||||
},
|
||||
}}"#;
|
||||
test_settings
|
||||
.set_user_settings(user_settings_content, cx)
|
||||
.unwrap();
|
||||
cx.set_global(test_settings);
|
||||
AssistantSettings::register(cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let assistant_settings = cx.update(|cx| AssistantSettings::get_global(cx).clone());
|
||||
assert!(assistant_settings.enabled);
|
||||
assert!(!assistant_settings.using_outdated_settings_version);
|
||||
assert_eq!(assistant_settings.default_model.model, "gpt-99");
|
||||
|
||||
cx.update_global::<SettingsStore, _>(|settings_store, cx| {
|
||||
settings_store.update_user_settings::<AssistantSettings>(cx, |settings| {
|
||||
*settings = AssistantSettingsContent {
|
||||
inner: Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AssistantSettingsTest {
|
||||
assistant: AssistantSettingsContent,
|
||||
agent: Option<serde_json_lenient::Value>,
|
||||
}
|
||||
|
||||
let assistant_settings: AssistantSettingsTest = serde_json::from_value(settings).unwrap();
|
||||
assert!(assistant_settings.agent.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1245,7 +1245,7 @@ impl EditAgentTest {
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
crate::init(client.http_client(), cx);
|
||||
});
|
||||
|
||||
|
||||
@@ -637,7 +637,7 @@ impl ToolCard for EditFileToolCard {
|
||||
.p_3()
|
||||
.gap_1()
|
||||
.border_t_1()
|
||||
.rounded_md()
|
||||
.rounded_b_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus};
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use gpui::{
|
||||
@@ -38,6 +38,12 @@ pub struct FindPathToolInput {
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct FindPathToolOutput {
|
||||
glob: String,
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
const RESULTS_PER_PAGE: usize = 50;
|
||||
|
||||
pub struct FindPathTool;
|
||||
@@ -111,10 +117,18 @@ impl Tool for FindPathTool {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let output = FindPathToolOutput {
|
||||
glob,
|
||||
paths: matches.clone(),
|
||||
};
|
||||
|
||||
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
|
||||
write!(&mut message, "\n{}", mat.display()).unwrap();
|
||||
}
|
||||
Ok(message.into())
|
||||
Ok(ToolResultOutput {
|
||||
content: message,
|
||||
output: Some(serde_json::to_value(output)?),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -123,6 +137,18 @@ impl Tool for FindPathTool {
|
||||
card: Some(card.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_card(
|
||||
self: Arc<Self>,
|
||||
output: serde_json::Value,
|
||||
_project: Entity<Project>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<assistant_tool::AnyToolCard> {
|
||||
let output = serde_json::from_value::<FindPathToolOutput>(output).ok()?;
|
||||
let card = cx.new(|_| FindPathToolCard::from_output(output));
|
||||
Some(card.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
|
||||
@@ -180,6 +206,15 @@ impl FindPathToolCard {
|
||||
_receiver_task: Some(_receiver_task),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_output(output: FindPathToolOutput) -> Self {
|
||||
Self {
|
||||
glob: output.glob,
|
||||
paths: output.paths,
|
||||
expanded: false,
|
||||
_receiver_task: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for FindPathToolCard {
|
||||
|
||||
@@ -2,13 +2,18 @@ use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
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 gpui::{
|
||||
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use language::LineEnding;
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use project::{Project, terminals::TerminalKind};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
@@ -17,6 +22,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use util::{
|
||||
get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
|
||||
@@ -125,8 +131,12 @@ impl Tool for TerminalTool {
|
||||
Err(err) => return Task::ready(Err(err)).into(),
|
||||
};
|
||||
let program = self.determine_shell.clone();
|
||||
let command = format!("({}) </dev/null", input.command);
|
||||
let args = vec!["-c".into(), command.clone()];
|
||||
let command = if cfg!(windows) {
|
||||
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
|
||||
} else {
|
||||
format!("({}) </dev/null", input.command)
|
||||
};
|
||||
let args = vec!["-c".into(), command];
|
||||
let cwd = working_dir.clone();
|
||||
let env = match &working_dir {
|
||||
Some(dir) => project.update(cx, |project, cx| {
|
||||
@@ -211,8 +221,21 @@ impl Tool for TerminalTool {
|
||||
}
|
||||
});
|
||||
|
||||
let command_markdown = cx.new(|cx| {
|
||||
Markdown::new(
|
||||
format!("```bash\n{}\n```", input.command).into(),
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let card = cx.new(|cx| {
|
||||
TerminalToolCard::new(input.command.clone(), working_dir.clone(), cx.entity_id())
|
||||
TerminalToolCard::new(
|
||||
command_markdown.clone(),
|
||||
working_dir.clone(),
|
||||
cx.entity_id(),
|
||||
)
|
||||
});
|
||||
|
||||
let output = cx.spawn({
|
||||
@@ -388,7 +411,7 @@ fn working_dir(
|
||||
}
|
||||
|
||||
struct TerminalToolCard {
|
||||
input_command: String,
|
||||
input_command: Entity<Markdown>,
|
||||
working_dir: Option<PathBuf>,
|
||||
entity_id: EntityId,
|
||||
exit_status: Option<ExitStatus>,
|
||||
@@ -404,7 +427,11 @@ struct TerminalToolCard {
|
||||
}
|
||||
|
||||
impl TerminalToolCard {
|
||||
pub fn new(input_command: String, working_dir: Option<PathBuf>, entity_id: EntityId) -> Self {
|
||||
pub fn new(
|
||||
input_command: Entity<Markdown>,
|
||||
working_dir: Option<PathBuf>,
|
||||
entity_id: EntityId,
|
||||
) -> Self {
|
||||
Self {
|
||||
input_command,
|
||||
working_dir,
|
||||
@@ -427,7 +454,7 @@ impl ToolCard for TerminalToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
@@ -571,11 +598,25 @@ impl ToolCard for TerminalToolCard {
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex().p_2().gap_0p5().bg(header_bg).child(header).child(
|
||||
Label::new(self.input_command.clone())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_0p5()
|
||||
.bg(header_bg)
|
||||
.text_xs()
|
||||
.child(header)
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
self.input_command.clone(),
|
||||
markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: true,
|
||||
border: false,
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(self.preview_expanded && !should_hide_terminal, |this| {
|
||||
this.child(
|
||||
@@ -594,6 +635,27 @@ impl ToolCard for TerminalToolCard {
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let buffer_font_size = TextSize::Default.rems(cx);
|
||||
let mut text_style = window.text_style();
|
||||
|
||||
text_style.refine(&TextStyleRefinement {
|
||||
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(buffer_font_size.into()),
|
||||
color: Some(cx.theme().colors().text),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
MarkdownStyle {
|
||||
base_text_style: text_style.clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use editor::EditorSettings;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{sync::Arc, time::Duration};
|
||||
use crate::schema::json_schema_for;
|
||||
use crate::ui::ToolCallCardHeader;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
@@ -73,9 +73,11 @@ impl Tool for WebSearchTool {
|
||||
let search_task = search_task.clone();
|
||||
async move {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err))?;
|
||||
serde_json::to_string(&response)
|
||||
.context("Failed to serialize search results")
|
||||
.map(Into::into)
|
||||
Ok(ToolResultOutput {
|
||||
content: serde_json::to_string(&response)
|
||||
.context("Failed to serialize search results")?,
|
||||
output: Some(serde_json::to_value(response)?),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -84,6 +86,18 @@ impl Tool for WebSearchTool {
|
||||
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_card(
|
||||
self: Arc<Self>,
|
||||
output: serde_json::Value,
|
||||
_project: Entity<Project>,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<assistant_tool::AnyToolCard> {
|
||||
let output = serde_json::from_value::<WebSearchResponse>(output).ok()?;
|
||||
let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx));
|
||||
Some(card.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
|
||||
@@ -38,6 +38,7 @@ pub enum Model {
|
||||
AmazonNovaLite,
|
||||
AmazonNovaMicro,
|
||||
AmazonNovaPro,
|
||||
AmazonNovaPremier,
|
||||
// AI21 models
|
||||
AI21J2GrandeInstruct,
|
||||
AI21J2JumboInstruct,
|
||||
@@ -72,6 +73,10 @@ pub enum Model {
|
||||
MistralMixtral8x7BInstructV0,
|
||||
MistralMistralLarge2402V1,
|
||||
MistralMistralSmall2402V1,
|
||||
MistralPixtralLarge2502V1,
|
||||
// Writer models
|
||||
PalmyraWriterX5,
|
||||
PalmyraWriterX4,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
@@ -120,6 +125,7 @@ impl Model {
|
||||
Model::AmazonNovaLite => "amazon.nova-lite-v1:0",
|
||||
Model::AmazonNovaMicro => "amazon.nova-micro-v1:0",
|
||||
Model::AmazonNovaPro => "amazon.nova-pro-v1:0",
|
||||
Model::AmazonNovaPremier => "amazon.nova-premier-v1:0",
|
||||
Model::DeepSeekR1 => "us.deepseek.r1-v1:0",
|
||||
Model::AI21J2GrandeInstruct => "ai21.j2-grande-instruct",
|
||||
Model::AI21J2JumboInstruct => "ai21.j2-jumbo-instruct",
|
||||
@@ -149,6 +155,9 @@ impl Model {
|
||||
Model::MistralMixtral8x7BInstructV0 => "mistral.mixtral-8x7b-instruct-v0:1",
|
||||
Model::MistralMistralLarge2402V1 => "mistral.mistral-large-2402-v1:0",
|
||||
Model::MistralMistralSmall2402V1 => "mistral.mistral-small-2402-v1:0",
|
||||
Model::MistralPixtralLarge2502V1 => "mistral.pixtral-large-2502-v1:0",
|
||||
Model::PalmyraWriterX4 => "writer.palmyra-x4-v1:0",
|
||||
Model::PalmyraWriterX5 => "writer.palmyra-x5-v1:0",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -166,6 +175,7 @@ impl Model {
|
||||
Self::AmazonNovaLite => "Amazon Nova Lite",
|
||||
Self::AmazonNovaMicro => "Amazon Nova Micro",
|
||||
Self::AmazonNovaPro => "Amazon Nova Pro",
|
||||
Self::AmazonNovaPremier => "Amazon Nova Premier",
|
||||
Self::DeepSeekR1 => "DeepSeek R1",
|
||||
Self::AI21J2GrandeInstruct => "AI21 Jurassic2 Grande Instruct",
|
||||
Self::AI21J2JumboInstruct => "AI21 Jurassic2 Jumbo Instruct",
|
||||
@@ -195,6 +205,9 @@ impl Model {
|
||||
Self::MistralMixtral8x7BInstructV0 => "Mistral Mixtral 8x7B Instruct V0",
|
||||
Self::MistralMistralLarge2402V1 => "Mistral Large 2402 V1",
|
||||
Self::MistralMistralSmall2402V1 => "Mistral Small 2402 V1",
|
||||
Self::MistralPixtralLarge2502V1 => "Pixtral Large 25.02 V1",
|
||||
Self::PalmyraWriterX5 => "Writer Palmyra X5",
|
||||
Self::PalmyraWriterX4 => "Writer Palmyra X4",
|
||||
Self::Custom {
|
||||
display_name, name, ..
|
||||
} => display_name.as_deref().unwrap_or(name),
|
||||
@@ -208,8 +221,11 @@ impl Model {
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3_5Haiku
|
||||
| Self::Claude3_7Sonnet => 200_000,
|
||||
Self::AmazonNovaPremier => 1_000_000,
|
||||
Self::PalmyraWriterX5 => 1_000_000,
|
||||
Self::PalmyraWriterX4 => 128_000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
_ => 200_000,
|
||||
_ => 128_000,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +233,7 @@ impl Model {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
|
||||
Self::Claude3_5SonnetV2 => 8_192,
|
||||
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => max_output_tokens.unwrap_or(4_096),
|
||||
@@ -252,7 +268,10 @@ impl Model {
|
||||
| Self::Claude3_5Haiku => true,
|
||||
|
||||
// Amazon Nova models (all support tool use)
|
||||
Self::AmazonNovaPro | Self::AmazonNovaLite | Self::AmazonNovaMicro => true,
|
||||
Self::AmazonNovaPremier
|
||||
| Self::AmazonNovaPro
|
||||
| Self::AmazonNovaLite
|
||||
| Self::AmazonNovaMicro => true,
|
||||
|
||||
// AI21 Jamba 1.5 models support tool use
|
||||
Self::AI21Jamba15LargeV1 | Self::AI21Jamba15MiniV1 => true,
|
||||
@@ -305,8 +324,11 @@ impl Model {
|
||||
|
||||
// Models available only in US
|
||||
(Model::Claude3Opus, "us")
|
||||
| (Model::Claude3_5Haiku, "us")
|
||||
| (Model::Claude3_7Sonnet, "us")
|
||||
| (Model::Claude3_7SonnetThinking, "us") => {
|
||||
| (Model::Claude3_7SonnetThinking, "us")
|
||||
| (Model::AmazonNovaPremier, "us")
|
||||
| (Model::MistralPixtralLarge2502V1, "us") => {
|
||||
Ok(format!("{}.{}", region_group, model_id))
|
||||
}
|
||||
|
||||
@@ -340,6 +362,12 @@ impl Model {
|
||||
Ok(format!("{}.{}", region_group, model_id))
|
||||
}
|
||||
|
||||
// Writer models only available in the US
|
||||
(Model::PalmyraWriterX4, "us") | (Model::PalmyraWriterX5, "us") => {
|
||||
// They have some goofiness
|
||||
Ok(format!("{}.{}", region_group, model_id))
|
||||
}
|
||||
|
||||
// Any other combination is not supported
|
||||
_ => Ok(self.id().into()),
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ use telemetry::Telemetry;
|
||||
use thiserror::Error;
|
||||
use tokio::net::TcpStream;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use util::{ConnectionResult, ResultExt};
|
||||
|
||||
pub use rpc::*;
|
||||
pub use telemetry_events::Event;
|
||||
@@ -151,9 +151,19 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||
let client = client.clone();
|
||||
move |_: &SignIn, cx| {
|
||||
if let Some(client) = client.upgrade() {
|
||||
cx.spawn(async move |cx| {
|
||||
client.authenticate_and_connect(true, &cx).log_err().await
|
||||
})
|
||||
cx.spawn(
|
||||
async move |cx| match client.authenticate_and_connect(true, &cx).await {
|
||||
ConnectionResult::Timeout => {
|
||||
log::error!("Initial authentication timed out");
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::error!("Initial authentication connection reset");
|
||||
}
|
||||
ConnectionResult::Result(r) => {
|
||||
r.log_err();
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -658,7 +668,7 @@ impl Client {
|
||||
state._reconnect_task = None;
|
||||
}
|
||||
Status::ConnectionLost => {
|
||||
let this = self.clone();
|
||||
let client = self.clone();
|
||||
state._reconnect_task = Some(cx.spawn(async move |cx| {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
@@ -666,10 +676,25 @@ impl Client {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
||||
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||
while let Err(error) = this.authenticate_and_connect(true, &cx).await {
|
||||
log::error!("failed to connect {}", error);
|
||||
if matches!(*this.status().borrow(), Status::ConnectionError) {
|
||||
this.set_status(
|
||||
loop {
|
||||
match client.authenticate_and_connect(true, &cx).await {
|
||||
ConnectionResult::Timeout => {
|
||||
log::error!("client connect attempt timed out")
|
||||
}
|
||||
ConnectionResult::ConnectionReset => {
|
||||
log::error!("client connect attempt reset")
|
||||
}
|
||||
ConnectionResult::Result(r) => {
|
||||
if let Err(error) = r {
|
||||
log::error!("failed to connect: {error}");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(*client.status().borrow(), Status::ConnectionError) {
|
||||
client.set_status(
|
||||
Status::ReconnectionError {
|
||||
next_reconnection: Instant::now() + delay,
|
||||
},
|
||||
@@ -827,7 +852,7 @@ impl Client {
|
||||
self: &Arc<Self>,
|
||||
try_provider: bool,
|
||||
cx: &AsyncApp,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> ConnectionResult<()> {
|
||||
let was_disconnected = match *self.status().borrow() {
|
||||
Status::SignedOut => true,
|
||||
Status::ConnectionError
|
||||
@@ -836,9 +861,14 @@ impl Client {
|
||||
| Status::Reauthenticating { .. }
|
||||
| Status::ReconnectionError { .. } => false,
|
||||
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
|
||||
return Ok(());
|
||||
return ConnectionResult::Result(Ok(()));
|
||||
}
|
||||
Status::UpgradeRequired => {
|
||||
return ConnectionResult::Result(
|
||||
Err(EstablishConnectionError::UpgradeRequired)
|
||||
.context("client auth and connect"),
|
||||
);
|
||||
}
|
||||
Status::UpgradeRequired => return Err(EstablishConnectionError::UpgradeRequired)?,
|
||||
};
|
||||
if was_disconnected {
|
||||
self.set_status(Status::Authenticating, cx);
|
||||
@@ -862,12 +892,12 @@ impl Client {
|
||||
Ok(creds) => credentials = Some(creds),
|
||||
Err(err) => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
return Err(err);
|
||||
return ConnectionResult::Result(Err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = status_rx.next().fuse() => {
|
||||
return Err(anyhow!("authentication canceled"));
|
||||
return ConnectionResult::Result(Err(anyhow!("authentication canceled")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -892,10 +922,10 @@ impl Client {
|
||||
}
|
||||
|
||||
futures::select_biased! {
|
||||
result = self.set_connection(conn, cx).fuse() => result,
|
||||
result = self.set_connection(conn, cx).fuse() => ConnectionResult::Result(result.context("client auth and connect")),
|
||||
_ = timeout => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(anyhow!("timed out waiting on hello message from server"))
|
||||
ConnectionResult::Timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -907,22 +937,22 @@ impl Client {
|
||||
self.authenticate_and_connect(false, cx).await
|
||||
} else {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(EstablishConnectionError::Unauthorized)?
|
||||
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
|
||||
}
|
||||
}
|
||||
Err(EstablishConnectionError::UpgradeRequired) => {
|
||||
self.set_status(Status::UpgradeRequired, cx);
|
||||
Err(EstablishConnectionError::UpgradeRequired)?
|
||||
ConnectionResult::Result(Err(EstablishConnectionError::UpgradeRequired).context("client auth and connect"))
|
||||
}
|
||||
Err(error) => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(error)?
|
||||
ConnectionResult::Result(Err(error).context("client auth and connect"))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut timeout => {
|
||||
self.set_status(Status::ConnectionError, cx);
|
||||
Err(anyhow!("timed out trying to establish connection"))
|
||||
ConnectionResult::Timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -938,10 +968,7 @@ impl Client {
|
||||
|
||||
let peer_id = async {
|
||||
log::debug!("waiting for server hello");
|
||||
let message = incoming
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("no hello message received"))?;
|
||||
let message = incoming.next().await.context("no hello message received")?;
|
||||
log::debug!("got server hello");
|
||||
let hello_message_type_name = message.payload_type_name().to_string();
|
||||
let hello = message
|
||||
@@ -1743,7 +1770,7 @@ mod tests {
|
||||
status.next().await,
|
||||
Some(Status::ConnectionError { .. })
|
||||
));
|
||||
auth_and_connect.await.unwrap_err();
|
||||
auth_and_connect.await.into_response().unwrap_err();
|
||||
|
||||
// Allow the connection to be established.
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
@@ -107,6 +107,7 @@ impl FakeServer {
|
||||
client
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
|
||||
server
|
||||
|
||||
@@ -1137,6 +1137,12 @@ async fn handle_customer_subscription_event(
|
||||
.await?;
|
||||
}
|
||||
|
||||
// When the user's subscription changes, push down any changes to their plan.
|
||||
rpc_server
|
||||
.update_plan_for_user(billing_customer.user_id)
|
||||
.await
|
||||
.trace_err();
|
||||
|
||||
// When the user's subscription changes, we want to refresh their LLM tokens
|
||||
// to either grant/revoke access.
|
||||
rpc_server
|
||||
@@ -1274,7 +1280,7 @@ async fn get_current_usage(
|
||||
subscription
|
||||
.kind
|
||||
.map(Into::into)
|
||||
.unwrap_or(zed_llm_client::Plan::Free)
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree)
|
||||
});
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
|
||||
@@ -99,7 +99,7 @@ impl From<SubscriptionKind> for zed_llm_client::Plan {
|
||||
match value {
|
||||
SubscriptionKind::ZedPro => Self::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Self::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => Self::Free,
|
||||
SubscriptionKind::ZedFree => Self::ZedFree,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,18 +25,12 @@ pub struct LlmTokenClaims {
|
||||
pub is_staff: bool,
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
pub bypass_account_age_check: bool,
|
||||
#[serde(default)]
|
||||
pub use_llm_request_queue: bool,
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub has_extended_trial: bool,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
#[serde(default)]
|
||||
pub subscription_period: (NaiveDateTime, NaiveDateTime),
|
||||
pub enable_model_request_overages: bool,
|
||||
#[serde(default)]
|
||||
pub model_request_overages_spend_limit_in_cents: u32,
|
||||
#[serde(default)]
|
||||
pub can_use_web_search_tool: bool,
|
||||
}
|
||||
|
||||
@@ -57,6 +51,23 @@ impl LlmTokenClaims {
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("no LLM API secret"))?;
|
||||
|
||||
let plan = if is_staff {
|
||||
Plan::ZedPro
|
||||
} else {
|
||||
subscription
|
||||
.as_ref()
|
||||
.and_then(|subscription| subscription.kind)
|
||||
.map_or(Plan::ZedFree, |kind| match kind {
|
||||
SubscriptionKind::ZedFree => Plan::ZedFree,
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
})
|
||||
};
|
||||
let subscription_period =
|
||||
billing_subscription::Model::current_period(subscription, is_staff)
|
||||
.map(|(start, end)| (start.naive_utc(), end.naive_utc()))
|
||||
.ok_or_else(|| anyhow!("A plan is required to use Zed's hosted models or edit predictions. Visit https://zed.dev/account to get started."))?;
|
||||
|
||||
let now = Utc::now();
|
||||
let claims = Self {
|
||||
iat: now.timestamp() as u64,
|
||||
@@ -76,26 +87,11 @@ impl LlmTokenClaims {
|
||||
.any(|flag| flag == "bypass-account-age-check"),
|
||||
can_use_web_search_tool: true,
|
||||
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
|
||||
plan: if is_staff {
|
||||
Plan::ZedPro
|
||||
} else {
|
||||
subscription
|
||||
.as_ref()
|
||||
.and_then(|subscription| subscription.kind)
|
||||
.map_or(Plan::Free, |kind| match kind {
|
||||
SubscriptionKind::ZedFree => Plan::Free,
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
})
|
||||
},
|
||||
plan,
|
||||
has_extended_trial: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
|
||||
subscription_period: billing_subscription::Model::current_period(
|
||||
subscription,
|
||||
is_staff,
|
||||
)
|
||||
.map(|(start, end)| (start.naive_utc(), end.naive_utc())),
|
||||
subscription_period,
|
||||
enable_model_request_overages: billing_preferences
|
||||
.as_ref()
|
||||
.map_or(false, |preferences| {
|
||||
|
||||
@@ -2,6 +2,7 @@ mod connection_pool;
|
||||
|
||||
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};
|
||||
use crate::{
|
||||
AppState, Error, Result, auth,
|
||||
@@ -67,7 +68,7 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::{MutexGuard, Semaphore, watch};
|
||||
use tokio::sync::{Semaphore, watch};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{
|
||||
Instrument,
|
||||
@@ -166,29 +167,6 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn current_plan(&self, db: &MutexGuard<'_, DbHandle>) -> anyhow::Result<proto::Plan> {
|
||||
if self.is_staff() {
|
||||
return Ok(proto::Plan::ZedPro);
|
||||
}
|
||||
|
||||
let user_id = self.user_id();
|
||||
|
||||
let subscription = db.get_active_billing_subscription(user_id).await?;
|
||||
let subscription_kind = subscription.and_then(|subscription| subscription.kind);
|
||||
|
||||
let plan = if let Some(subscription_kind) = subscription_kind {
|
||||
match subscription_kind {
|
||||
SubscriptionKind::ZedPro => proto::Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => proto::Plan::Free,
|
||||
}
|
||||
} else {
|
||||
proto::Plan::Free
|
||||
};
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn user_id(&self) -> UserId {
|
||||
match &self.principal {
|
||||
Principal::User(user) => user.id,
|
||||
@@ -953,6 +931,32 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_plan_for_user(self: &Arc<Self>, user_id: UserId) -> Result<()> {
|
||||
let user = self
|
||||
.app_state
|
||||
.db
|
||||
.get_user_by_id(user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let update_user_plan = make_update_user_plan_message(
|
||||
&self.app_state.db,
|
||||
self.app_state.llm_db.clone(),
|
||||
user_id,
|
||||
user.admin,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let pool = self.connection_pool.lock();
|
||||
for connection_id in pool.user_connection_ids(user_id) {
|
||||
self.peer
|
||||
.send(connection_id, update_user_plan.clone())
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_llm_tokens_for_user(self: &Arc<Self>, user_id: UserId) {
|
||||
let pool = self.connection_pool.lock();
|
||||
for connection_id in pool.user_connection_ids(user_id) {
|
||||
@@ -2688,21 +2692,43 @@ fn should_auto_subscribe_to_channels(version: ZedVersion) -> bool {
|
||||
version.0.minor() < 139
|
||||
}
|
||||
|
||||
async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
async fn current_plan(db: &Arc<Database>, user_id: UserId, is_staff: bool) -> Result<proto::Plan> {
|
||||
if is_staff {
|
||||
return Ok(proto::Plan::ZedPro);
|
||||
}
|
||||
|
||||
let subscription = db.get_active_billing_subscription(user_id).await?;
|
||||
let subscription_kind = subscription.and_then(|subscription| subscription.kind);
|
||||
|
||||
let plan = if let Some(subscription_kind) = subscription_kind {
|
||||
match subscription_kind {
|
||||
SubscriptionKind::ZedPro => proto::Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => proto::Plan::Free,
|
||||
}
|
||||
} else {
|
||||
proto::Plan::Free
|
||||
};
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
async fn make_update_user_plan_message(
|
||||
db: &Arc<Database>,
|
||||
llm_db: Option<Arc<LlmDatabase>>,
|
||||
user_id: UserId,
|
||||
is_staff: bool,
|
||||
) -> Result<proto::UpdateUserPlan> {
|
||||
let feature_flags = db.get_user_flags(user_id).await?;
|
||||
let plan = session.current_plan(&db).await?;
|
||||
let plan = current_plan(db, user_id, is_staff).await?;
|
||||
let billing_customer = db.get_billing_customer_by_user_id(user_id).await?;
|
||||
let billing_preferences = db.get_billing_preferences(user_id).await?;
|
||||
|
||||
let (subscription_period, usage) = if let Some(llm_db) = session.app_state.llm_db.clone() {
|
||||
let (subscription_period, usage) = if let Some(llm_db) = llm_db {
|
||||
let subscription = db.get_active_billing_subscription(user_id).await?;
|
||||
|
||||
let subscription_period = crate::db::billing_subscription::Model::current_period(
|
||||
subscription,
|
||||
session.is_staff(),
|
||||
);
|
||||
let subscription_period =
|
||||
crate::db::billing_subscription::Model::current_period(subscription, is_staff);
|
||||
|
||||
let usage = if let Some((period_start_at, period_end_at)) = subscription_period {
|
||||
llm_db
|
||||
@@ -2717,92 +2743,92 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
session.connection_id,
|
||||
proto::UpdateUserPlan {
|
||||
plan: plan.into(),
|
||||
trial_started_at: billing_customer
|
||||
.and_then(|billing_customer| billing_customer.trial_started_at)
|
||||
.map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64),
|
||||
is_usage_based_billing_enabled: if session.is_staff() {
|
||||
Some(true)
|
||||
} else {
|
||||
billing_preferences
|
||||
.map(|preferences| preferences.model_request_overages_enabled)
|
||||
},
|
||||
subscription_period: subscription_period.map(|(started_at, ended_at)| {
|
||||
proto::SubscriptionPeriod {
|
||||
started_at: started_at.timestamp() as u64,
|
||||
ended_at: ended_at.timestamp() as u64,
|
||||
}
|
||||
}),
|
||||
usage: usage.map(|usage| {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::Free,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
Ok(proto::UpdateUserPlan {
|
||||
plan: plan.into(),
|
||||
trial_started_at: billing_customer
|
||||
.and_then(|billing_customer| billing_customer.trial_started_at)
|
||||
.map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64),
|
||||
is_usage_based_billing_enabled: if is_staff {
|
||||
Some(true)
|
||||
} else {
|
||||
billing_preferences.map(|preferences| preferences.model_request_overages_enabled)
|
||||
},
|
||||
subscription_period: subscription_period.map(|(started_at, ended_at)| {
|
||||
proto::SubscriptionPeriod {
|
||||
started_at: started_at.timestamp() as u64,
|
||||
ended_at: ended_at.timestamp() as u64,
|
||||
}
|
||||
}),
|
||||
usage: usage.map(|usage| {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
||||
&& feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||
{
|
||||
1_000
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
||||
&& feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||
{
|
||||
1_000
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
zed_llm_client::UsageLimit::Limited(limit)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
|
||||
};
|
||||
|
||||
zed_llm_client::UsageLimit::Limited(limit)
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: usage.model_requests as u32,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
zed_llm_client::UsageLimit::Unlimited
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: usage.model_requests as u32,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(
|
||||
proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
},
|
||||
)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(
|
||||
proto::usage_limit::Unlimited {},
|
||||
)
|
||||
}
|
||||
}),
|
||||
}),
|
||||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(
|
||||
proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
},
|
||||
)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(
|
||||
proto::usage_limit::Unlimited {},
|
||||
)
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
},
|
||||
)
|
||||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
|
||||
let update_user_plan = make_update_user_plan_message(
|
||||
&db.0,
|
||||
session.app_state.llm_db.clone(),
|
||||
user_id,
|
||||
session.is_staff(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
session
|
||||
.peer
|
||||
.send(session.connection_id, update_user_plan)
|
||||
.trace_err();
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -248,6 +248,8 @@ impl StripeBilling {
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.payment_method_collection =
|
||||
Some(stripe::CheckoutSessionPaymentMethodCollection::IfRequired);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
|
||||
@@ -1740,6 +1740,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
fake_language_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.into_response()
|
||||
.expect("inlay refresh request failed");
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -1930,6 +1931,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
fake_language_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.into_response()
|
||||
.expect("inlay refresh request failed");
|
||||
executor.run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
|
||||
@@ -1253,6 +1253,7 @@ async fn test_calls_on_multiple_connections(
|
||||
client_b1
|
||||
.authenticate_and_connect(false, &cx_b1.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
|
||||
// User B hangs up, and user A calls them again.
|
||||
@@ -1633,6 +1634,7 @@ async fn test_project_reconnect(
|
||||
client_a
|
||||
.authenticate_and_connect(false, &cx_a.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -1761,6 +1763,7 @@ async fn test_project_reconnect(
|
||||
client_b
|
||||
.authenticate_and_connect(false, &cx_b.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
@@ -4317,6 +4320,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
token: lsp::NumberOrString::String("the-disk-based-token".to_string()),
|
||||
@@ -5699,6 +5703,7 @@ async fn test_contacts(
|
||||
client_c
|
||||
.authenticate_and_connect(false, &cx_c.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
@@ -6229,6 +6234,7 @@ async fn test_contact_requests(
|
||||
client
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ impl TestServer {
|
||||
client
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
|
||||
let client = TestClient {
|
||||
|
||||
@@ -42,6 +42,7 @@ futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
picker.workspace = true
|
||||
|
||||
@@ -2227,6 +2227,7 @@ impl CollabPanel {
|
||||
client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.await
|
||||
.into_response()
|
||||
.notify_async_err(cx);
|
||||
})
|
||||
.detach()
|
||||
|
||||
@@ -646,10 +646,20 @@ impl Render for NotificationPanel {
|
||||
let client = client.clone();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
client
|
||||
match client
|
||||
.authenticate_and_connect(true, &cx)
|
||||
.log_err()
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
util::ConnectionResult::Timeout => {
|
||||
log::error!("Connection timeout");
|
||||
}
|
||||
util::ConnectionResult::ConnectionReset => {
|
||||
log::error!("Connection reset");
|
||||
}
|
||||
util::ConnectionResult::Result(r) => {
|
||||
r.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
[package]
|
||||
name = "component_preview"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/component_preview.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
languages.workspace = true
|
||||
log.workspace = true
|
||||
notifications.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
serde.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -14,7 +14,6 @@ doctest = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars"]
|
||||
test-support = [
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
@@ -43,16 +42,15 @@ node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
strum.workspace = true
|
||||
task.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
@@ -5,7 +5,7 @@ mod sign_in;
|
||||
|
||||
use crate::sign_in::initiate_sign_in_within_workspace;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{HashMap, HashSet};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
|
||||
@@ -531,11 +531,15 @@ impl Copilot {
|
||||
.request::<request::CheckStatus>(request::CheckStatusParams {
|
||||
local_checks_only: false,
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: check status")?;
|
||||
|
||||
server
|
||||
.request::<request::SetEditorInfo>(editor_info)
|
||||
.await?;
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: set editor info")?;
|
||||
|
||||
anyhow::Ok((server, status))
|
||||
};
|
||||
@@ -581,7 +585,9 @@ impl Copilot {
|
||||
.request::<request::SignInInitiate>(
|
||||
request::SignInInitiateParams {},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot sign-in")?;
|
||||
match sign_in {
|
||||
request::SignInInitiateResult::AlreadySignedIn { user } => {
|
||||
Ok(request::SignInStatus::Ok { user: Some(user) })
|
||||
@@ -609,7 +615,9 @@ impl Copilot {
|
||||
user_code: flow.user_code,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: sign in confirm")?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
@@ -656,7 +664,9 @@ impl Copilot {
|
||||
cx.background_spawn(async move {
|
||||
server
|
||||
.request::<request::SignOut>(request::SignOutParams {})
|
||||
.await?;
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: sign in confirm")?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
@@ -873,7 +883,10 @@ impl Copilot {
|
||||
uuid: completion.uuid.clone(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request.await?;
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify accepted")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -897,7 +910,10 @@ impl Copilot {
|
||||
.collect(),
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
request.await?;
|
||||
request
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: notify rejected")?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -957,7 +973,9 @@ impl Copilot {
|
||||
version: version.try_into().unwrap(),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
.await
|
||||
.into_response()
|
||||
.context("copilot: get completions")?;
|
||||
let completions = result
|
||||
.completions
|
||||
.into_iter()
|
||||
|
||||
@@ -9,13 +9,20 @@ use fs::Fs;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use gpui::{App, AsyncApp, Global, prelude::*};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use itertools::Itertools;
|
||||
use paths::home_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_dir;
|
||||
use strum::EnumIter;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
||||
pub const COPILOT_CHAT_MODELS_URL: &str = "https://api.githubcopilot.com/models";
|
||||
|
||||
// Copilot's base model; defined by Microsoft in premium requests table
|
||||
// This will be moved to the front of the Copilot model list, and will be used for
|
||||
// 'fast' requests (e.g. title generation)
|
||||
// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
|
||||
const DEFAULT_MODEL_ID: &str = "gpt-4.1";
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -25,132 +32,130 @@ pub enum Role {
|
||||
System,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
|
||||
pub enum Model {
|
||||
#[default]
|
||||
#[serde(alias = "gpt-4o", rename = "gpt-4o-2024-05-13")]
|
||||
Gpt4o,
|
||||
#[serde(alias = "gpt-4", rename = "gpt-4")]
|
||||
Gpt4,
|
||||
#[serde(alias = "gpt-4.1", rename = "gpt-4.1")]
|
||||
Gpt4_1,
|
||||
#[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")]
|
||||
Gpt3_5Turbo,
|
||||
#[serde(alias = "o1", rename = "o1")]
|
||||
O1,
|
||||
#[serde(alias = "o1-mini", rename = "o3-mini")]
|
||||
O3Mini,
|
||||
#[serde(alias = "o3", rename = "o3")]
|
||||
O3,
|
||||
#[serde(alias = "o4-mini", rename = "o4-mini")]
|
||||
O4Mini,
|
||||
#[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")]
|
||||
Claude3_5Sonnet,
|
||||
#[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")]
|
||||
Claude3_7Sonnet,
|
||||
#[serde(
|
||||
alias = "claude-3.7-sonnet-thought",
|
||||
rename = "claude-3.7-sonnet-thought"
|
||||
)]
|
||||
Claude3_7SonnetThinking,
|
||||
#[serde(alias = "gemini-2.0-flash", rename = "gemini-2.0-flash-001")]
|
||||
Gemini20Flash,
|
||||
#[serde(alias = "gemini-2.5-pro", rename = "gemini-2.5-pro")]
|
||||
Gemini25Pro,
|
||||
#[derive(Deserialize)]
|
||||
struct ModelSchema {
|
||||
#[serde(deserialize_with = "deserialize_models_skip_errors")]
|
||||
data: Vec<Model>,
|
||||
}
|
||||
|
||||
fn deserialize_models_skip_errors<'de, D>(deserializer: D) -> Result<Vec<Model>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let raw_values = Vec::<serde_json::Value>::deserialize(deserializer)?;
|
||||
let models = raw_values
|
||||
.into_iter()
|
||||
.filter_map(|value| match serde_json::from_value::<Model>(value) {
|
||||
Ok(model) => Some(model),
|
||||
Err(err) => {
|
||||
log::warn!("GitHub Copilot Chat model failed to deserialize: {:?}", err);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct Model {
|
||||
capabilities: ModelCapabilities,
|
||||
id: String,
|
||||
name: String,
|
||||
policy: Option<ModelPolicy>,
|
||||
vendor: ModelVendor,
|
||||
model_picker_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ModelCapabilities {
|
||||
family: String,
|
||||
#[serde(default)]
|
||||
limits: ModelLimits,
|
||||
supports: ModelSupportedFeatures,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ModelLimits {
|
||||
#[serde(default)]
|
||||
max_context_window_tokens: usize,
|
||||
#[serde(default)]
|
||||
max_output_tokens: usize,
|
||||
#[serde(default)]
|
||||
max_prompt_tokens: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ModelPolicy {
|
||||
state: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ModelSupportedFeatures {
|
||||
#[serde(default)]
|
||||
streaming: bool,
|
||||
#[serde(default)]
|
||||
tool_calls: bool,
|
||||
#[serde(default)]
|
||||
parallel_tool_calls: bool,
|
||||
#[serde(default)]
|
||||
vision: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub enum ModelVendor {
|
||||
// Azure OpenAI should have no functional difference from OpenAI in Copilot Chat
|
||||
#[serde(alias = "Azure OpenAI")]
|
||||
OpenAI,
|
||||
Google,
|
||||
Anthropic,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatMessageContent {
|
||||
#[serde(rename = "text")]
|
||||
Text { text: String },
|
||||
#[serde(rename = "image_url")]
|
||||
Image { image_url: ImageUrl },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
|
||||
pub struct ImageUrl {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn default_fast() -> Self {
|
||||
Self::Claude3_7Sonnet
|
||||
}
|
||||
|
||||
pub fn uses_streaming(&self) -> bool {
|
||||
match self {
|
||||
Self::Gpt4o
|
||||
| Self::Gpt4
|
||||
| Self::Gpt4_1
|
||||
| Self::Gpt3_5Turbo
|
||||
| Self::O3
|
||||
| Self::O4Mini
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking => true,
|
||||
Self::O3Mini | Self::O1 | Self::Gemini20Flash | Self::Gemini25Pro => false,
|
||||
}
|
||||
self.capabilities.supports.streaming
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
match id {
|
||||
"gpt-4o" => Ok(Self::Gpt4o),
|
||||
"gpt-4" => Ok(Self::Gpt4),
|
||||
"gpt-4.1" => Ok(Self::Gpt4_1),
|
||||
"gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo),
|
||||
"o1" => Ok(Self::O1),
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"o3" => Ok(Self::O3),
|
||||
"o4-mini" => Ok(Self::O4Mini),
|
||||
"claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet),
|
||||
"claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet),
|
||||
"claude-3.7-sonnet-thought" => Ok(Self::Claude3_7SonnetThinking),
|
||||
"gemini-2.0-flash-001" => Ok(Self::Gemini20Flash),
|
||||
"gemini-2.5-pro" => Ok(Self::Gemini25Pro),
|
||||
_ => Err(anyhow!("Invalid model id: {}", id)),
|
||||
}
|
||||
pub fn id(&self) -> &str {
|
||||
self.id.as_str()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Gpt3_5Turbo => "gpt-3.5-turbo",
|
||||
Self::Gpt4 => "gpt-4",
|
||||
Self::Gpt4_1 => "gpt-4.1",
|
||||
Self::Gpt4o => "gpt-4o",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::O3 => "o3",
|
||||
Self::O4Mini => "o4-mini",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Self::Claude3_7Sonnet => "claude-3-7-sonnet",
|
||||
Self::Claude3_7SonnetThinking => "claude-3.7-sonnet-thought",
|
||||
Self::Gemini20Flash => "gemini-2.0-flash-001",
|
||||
Self::Gemini25Pro => "gemini-2.5-pro",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Gpt3_5Turbo => "GPT-3.5",
|
||||
Self::Gpt4 => "GPT-4",
|
||||
Self::Gpt4_1 => "GPT-4.1",
|
||||
Self::Gpt4o => "GPT-4o",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O1 => "o1",
|
||||
Self::O3 => "o3",
|
||||
Self::O4Mini => "o4-mini",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
|
||||
Self::Gemini20Flash => "Gemini 2.0 Flash",
|
||||
Self::Gemini25Pro => "Gemini 2.5 Pro",
|
||||
}
|
||||
pub fn display_name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
pub fn max_token_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Gpt4o => 64_000,
|
||||
Self::Gpt4 => 32_768,
|
||||
Self::Gpt4_1 => 128_000,
|
||||
Self::Gpt3_5Turbo => 12_288,
|
||||
Self::O3Mini => 64_000,
|
||||
Self::O1 => 20_000,
|
||||
Self::O3 => 128_000,
|
||||
Self::O4Mini => 128_000,
|
||||
Self::Claude3_5Sonnet => 200_000,
|
||||
Self::Claude3_7Sonnet => 90_000,
|
||||
Self::Claude3_7SonnetThinking => 90_000,
|
||||
Self::Gemini20Flash => 128_000,
|
||||
Self::Gemini25Pro => 128_000,
|
||||
}
|
||||
self.capabilities.limits.max_prompt_tokens
|
||||
}
|
||||
|
||||
pub fn supports_tools(&self) -> bool {
|
||||
self.capabilities.supports.tool_calls
|
||||
}
|
||||
|
||||
pub fn vendor(&self) -> ModelVendor {
|
||||
self.vendor
|
||||
}
|
||||
|
||||
pub fn supports_vision(&self) -> bool {
|
||||
self.capabilities.supports.vision
|
||||
}
|
||||
|
||||
pub fn supports_parallel_tool_calls(&self) -> bool {
|
||||
self.capabilities.supports.parallel_tool_calls
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +165,7 @@ pub struct Request {
|
||||
pub n: usize,
|
||||
pub stream: bool,
|
||||
pub temperature: f32,
|
||||
pub model: Model,
|
||||
pub model: String,
|
||||
pub messages: Vec<ChatMessage>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tools: Vec<Tool>,
|
||||
@@ -198,7 +203,7 @@ pub enum ChatMessage {
|
||||
tool_calls: Vec<ToolCall>,
|
||||
},
|
||||
User {
|
||||
content: String,
|
||||
content: Vec<ChatMessageContent>,
|
||||
},
|
||||
System {
|
||||
content: String,
|
||||
@@ -306,6 +311,7 @@ impl Global for GlobalCopilotChat {}
|
||||
pub struct CopilotChat {
|
||||
oauth_token: Option<String>,
|
||||
api_token: Option<ApiToken>,
|
||||
models: Option<Vec<Model>>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
@@ -342,31 +348,56 @@ impl CopilotChat {
|
||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||
let dir_path = copilot_chat_config_dir();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
fs.clone(),
|
||||
dir_path.clone(),
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.oauth_token = oauth_token;
|
||||
cx.notify();
|
||||
});
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
async move |cx| {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
fs.clone(),
|
||||
dir_path.clone(),
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.oauth_token = oauth_token.clone();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(ref oauth_token) = oauth_token {
|
||||
let api_token = request_api_token(oauth_token, client.clone()).await?;
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.api_token = Some(api_token.clone());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})?;
|
||||
let models = get_models(api_token.api_key, client.clone()).await?;
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.models = Some(models);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
oauth_token: None,
|
||||
api_token: None,
|
||||
models: None,
|
||||
client,
|
||||
}
|
||||
}
|
||||
@@ -375,6 +406,10 @@ impl CopilotChat {
|
||||
self.oauth_token.is_some()
|
||||
}
|
||||
|
||||
pub fn models(&self) -> Option<&[Model]> {
|
||||
self.models.as_deref()
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
request: Request,
|
||||
mut cx: AsyncApp,
|
||||
@@ -409,6 +444,61 @@ impl CopilotChat {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
|
||||
let all_models = request_models(api_token, client).await?;
|
||||
|
||||
let mut models: Vec<Model> = all_models
|
||||
.into_iter()
|
||||
.filter(|model| {
|
||||
// Ensure user has access to the model; Policy is present only for models that must be
|
||||
// enabled in the GitHub dashboard
|
||||
model.model_picker_enabled
|
||||
&& model
|
||||
.policy
|
||||
.as_ref()
|
||||
.is_none_or(|policy| policy.state == "enabled")
|
||||
})
|
||||
// The first model from the API response, in any given family, appear to be the non-tagged
|
||||
// models, which are likely the best choice (e.g. gpt-4o rather than gpt-4o-2024-11-20)
|
||||
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
|
||||
.collect();
|
||||
|
||||
if let Some(default_model_position) =
|
||||
models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
|
||||
{
|
||||
let default_model = models.remove(default_model_position);
|
||||
models.insert(0, default_model);
|
||||
}
|
||||
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
.uri(COPILOT_CHAT_MODELS_URL)
|
||||
.header("Authorization", format!("Bearer {}", api_token))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
|
||||
let request = request_builder.body(AsyncBody::empty())?;
|
||||
|
||||
let mut response = client.send(request).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
let mut body = Vec::new();
|
||||
response.body_mut().read_to_end(&mut body).await?;
|
||||
|
||||
let body_str = std::str::from_utf8(&body)?;
|
||||
|
||||
let models = serde_json::from_str::<ModelSchema>(body_str)?.data;
|
||||
|
||||
Ok(models)
|
||||
} else {
|
||||
Err(anyhow!("Failed to request models: {}", response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
@@ -472,7 +562,8 @@ async fn stream_completion(
|
||||
)
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
.header("Copilot-Integration-Id", "vscode-chat")
|
||||
.header("Copilot-Vision-Request", "true");
|
||||
|
||||
let is_streaming = request.stream;
|
||||
|
||||
@@ -527,3 +618,82 @@ async fn stream_completion(
|
||||
Ok(futures::stream::once(async move { Ok(response) }).boxed())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_resilient_model_schema_deserialize() {
|
||||
let json = r#"{
|
||||
"data": [
|
||||
{
|
||||
"capabilities": {
|
||||
"family": "gpt-4",
|
||||
"limits": {
|
||||
"max_context_window_tokens": 32768,
|
||||
"max_output_tokens": 4096,
|
||||
"max_prompt_tokens": 32768
|
||||
},
|
||||
"object": "model_capabilities",
|
||||
"supports": { "streaming": true, "tool_calls": true },
|
||||
"tokenizer": "cl100k_base",
|
||||
"type": "chat"
|
||||
},
|
||||
"id": "gpt-4",
|
||||
"model_picker_enabled": false,
|
||||
"name": "GPT 4",
|
||||
"object": "model",
|
||||
"preview": false,
|
||||
"vendor": "Azure OpenAI",
|
||||
"version": "gpt-4-0613"
|
||||
},
|
||||
{
|
||||
"some-unknown-field": 123
|
||||
},
|
||||
{
|
||||
"capabilities": {
|
||||
"family": "claude-3.7-sonnet",
|
||||
"limits": {
|
||||
"max_context_window_tokens": 200000,
|
||||
"max_output_tokens": 16384,
|
||||
"max_prompt_tokens": 90000,
|
||||
"vision": {
|
||||
"max_prompt_image_size": 3145728,
|
||||
"max_prompt_images": 1,
|
||||
"supported_media_types": ["image/jpeg", "image/png", "image/webp"]
|
||||
}
|
||||
},
|
||||
"object": "model_capabilities",
|
||||
"supports": {
|
||||
"parallel_tool_calls": true,
|
||||
"streaming": true,
|
||||
"tool_calls": true,
|
||||
"vision": true
|
||||
},
|
||||
"tokenizer": "o200k_base",
|
||||
"type": "chat"
|
||||
},
|
||||
"id": "claude-3.7-sonnet",
|
||||
"model_picker_enabled": true,
|
||||
"name": "Claude 3.7 Sonnet",
|
||||
"object": "model",
|
||||
"policy": {
|
||||
"state": "enabled",
|
||||
"terms": "Enable access to the latest Claude 3.7 Sonnet model from Anthropic. [Learn more about how GitHub Copilot serves Claude 3.7 Sonnet](https://docs.github.com/copilot/using-github-copilot/using-claude-sonnet-in-github-copilot)."
|
||||
},
|
||||
"preview": false,
|
||||
"vendor": "Anthropic",
|
||||
"version": "claude-3.7-sonnet"
|
||||
}
|
||||
],
|
||||
"object": "list"
|
||||
}"#;
|
||||
|
||||
let schema: ModelSchema = serde_json::from_str(&json).unwrap();
|
||||
|
||||
assert_eq!(schema.data.len(), 2);
|
||||
assert_eq!(schema.data[0].id, "gpt-4");
|
||||
assert_eq!(schema.data[1].id, "claude-3.7-sonnet");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ impl CodeLldbDebugAdapter {
|
||||
if !launch.args.is_empty() {
|
||||
map.insert("args".into(), launch.args.clone().into());
|
||||
}
|
||||
|
||||
if !launch.env.is_empty() {
|
||||
map.insert("env".into(), launch.env_json());
|
||||
}
|
||||
if let Some(stop_on_entry) = config.stop_on_entry {
|
||||
map.insert("stopOnEntry".into(), stop_on_entry.into());
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ impl GdbDebugAdapter {
|
||||
map.insert("args".into(), launch.args.clone().into());
|
||||
}
|
||||
|
||||
if !launch.env.is_empty() {
|
||||
map.insert("env".into(), launch.env_json());
|
||||
}
|
||||
|
||||
if let Some(stop_on_entry) = config.stop_on_entry {
|
||||
map.insert(
|
||||
"stopAtBeginningOfMainSubprogram".into(),
|
||||
|
||||
@@ -19,7 +19,8 @@ impl GoDebugAdapter {
|
||||
dap::DebugRequest::Launch(launch_config) => json!({
|
||||
"program": launch_config.program,
|
||||
"cwd": launch_config.cwd,
|
||||
"args": launch_config.args
|
||||
"args": launch_config.args,
|
||||
"env": launch_config.env_json()
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ impl JsDebugAdapter {
|
||||
if !launch.args.is_empty() {
|
||||
map.insert("args".into(), launch.args.clone().into());
|
||||
}
|
||||
if !launch.env.is_empty() {
|
||||
map.insert("env".into(), launch.env_json());
|
||||
}
|
||||
|
||||
if let Some(stop_on_entry) = config.stop_on_entry {
|
||||
map.insert("stopOnEntry".into(), stop_on_entry.into());
|
||||
|
||||
@@ -29,6 +29,7 @@ impl PhpDebugAdapter {
|
||||
"program": launch_config.program,
|
||||
"cwd": launch_config.cwd,
|
||||
"args": launch_config.args,
|
||||
"env": launch_config.env_json(),
|
||||
"stopOnEntry": config.stop_on_entry.unwrap_or_default(),
|
||||
}),
|
||||
request: config.request.to_dap(),
|
||||
|
||||
@@ -32,6 +32,9 @@ impl PythonDebugAdapter {
|
||||
DebugRequest::Launch(launch) => {
|
||||
map.insert("program".into(), launch.program.clone().into());
|
||||
map.insert("args".into(), launch.args.clone().into());
|
||||
if !launch.env.is_empty() {
|
||||
map.insert("env".into(), launch.env_json());
|
||||
}
|
||||
|
||||
if let Some(stop_on_entry) = config.stop_on_entry {
|
||||
map.insert("stopOnEntry".into(), stop_on_entry.into());
|
||||
|
||||
@@ -62,7 +62,7 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
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 {
|
||||
let DebugRequest::Launch(launch) = definition.request.clone() else {
|
||||
anyhow::bail!("rdbg does not yet support attaching");
|
||||
};
|
||||
|
||||
@@ -71,12 +71,6 @@ impl DebugAdapter for RubyDebugAdapter {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
sysinfo.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
|
||||
@@ -22,7 +22,7 @@ use gpui::{
|
||||
|
||||
use language::Buffer;
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{Fs, WorktreeId};
|
||||
use project::{Fs, ProjectPath, WorktreeId};
|
||||
use project::{Project, debugger::session::ThreadStatus};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
@@ -291,7 +291,7 @@ impl DebugPanel {
|
||||
|
||||
let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
!session
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
@@ -997,7 +997,7 @@ impl DebugPanel {
|
||||
worktree_id: WorktreeId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
) -> Task<Result<ProjectPath>> {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
|
||||
@@ -1006,14 +1006,20 @@ impl DebugPanel {
|
||||
|
||||
let serialized_scenario = serde_json::to_value(scenario);
|
||||
|
||||
path.push(paths::local_debug_file_relative_path());
|
||||
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let serialized_scenario = serialized_scenario?;
|
||||
let path = path.as_path();
|
||||
let fs =
|
||||
workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||
|
||||
path.push(paths::local_settings_folder_relative_path());
|
||||
if !fs.is_dir(path.as_path()).await {
|
||||
fs.create_dir(path.as_path()).await?;
|
||||
}
|
||||
path.pop();
|
||||
|
||||
path.push(paths::local_debug_file_relative_path());
|
||||
let path = path.as_path();
|
||||
|
||||
if !fs.is_file(path).await {
|
||||
let content =
|
||||
serde_json::to_string_pretty(&serde_json::Value::Array(vec![
|
||||
@@ -1034,21 +1040,19 @@ impl DebugPanel {
|
||||
.await?;
|
||||
}
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(project_path) = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.project_path_for_absolute_path(&path, cx)
|
||||
{
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
Ok(project_path)
|
||||
} else {
|
||||
Task::ready(Err(anyhow!(
|
||||
Err(anyhow!(
|
||||
"Couldn't get project path for .zed/debug.json in active worktree"
|
||||
)))
|
||||
))
|
||||
}
|
||||
})?.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})?
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use collections::FxHashMap;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::Not,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use dap::{
|
||||
DapRegistry, DebugRequest,
|
||||
adapters::{DebugAdapterName, DebugTaskDefinition},
|
||||
@@ -13,26 +16,32 @@ use dap::{
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||
Subscription, TextStyle, WeakEntity,
|
||||
Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, LaunchRequest};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
|
||||
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
|
||||
relative, rems, v_flex,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconButton, IconName, IconSize,
|
||||
InteractiveElement, IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing,
|
||||
ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, ToggleState,
|
||||
Toggleable, Window, div, h_flex, relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, pane};
|
||||
|
||||
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
|
||||
|
||||
enum SaveScenarioState {
|
||||
Saving,
|
||||
Saved(ProjectPath),
|
||||
Failed(SharedString),
|
||||
}
|
||||
|
||||
pub(super) struct NewSessionModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
@@ -42,6 +51,7 @@ pub(super) struct NewSessionModal {
|
||||
custom_mode: Entity<CustomMode>,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
save_scenario_state: Option<SaveScenarioState>,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
@@ -125,6 +135,7 @@ impl NewSessionModal {
|
||||
debug_panel: debug_panel.downgrade(),
|
||||
workspace: workspace_handle,
|
||||
task_contexts,
|
||||
save_scenario_state: None,
|
||||
_subscriptions,
|
||||
}
|
||||
});
|
||||
@@ -219,7 +230,7 @@ impl NewSessionModal {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
anyhow::Result::<_, anyhow::Error>::Ok(())
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -379,6 +390,8 @@ impl Render for NewSessionModal {
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let this = cx.weak_entity().clone();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.w(rems(34.))
|
||||
@@ -484,42 +497,148 @@ impl Render for NewSessionModal {
|
||||
}
|
||||
}),
|
||||
),
|
||||
NewSessionMode::Custom => div().child(
|
||||
Button::new("new-session-modal-back", "Save to .zed/debug.json...")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let Some(save_scenario_task) = this
|
||||
.debugger
|
||||
.as_ref()
|
||||
.and_then(|debugger| this.debug_scenario(&debugger, cx))
|
||||
.zip(this.task_contexts.worktree())
|
||||
.and_then(|(scenario, worktree_id)| {
|
||||
this.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.save_scenario(
|
||||
&scenario,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
NewSessionMode::Custom => h_flex()
|
||||
.child(
|
||||
Button::new("new-session-modal-back", "Save to .zed/debug.json...")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let Some(save_scenario) = this
|
||||
.debugger
|
||||
.as_ref()
|
||||
.and_then(|debugger| this.debug_scenario(&debugger, cx))
|
||||
.zip(this.task_contexts.worktree())
|
||||
.and_then(|(scenario, worktree_id)| {
|
||||
this.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.save_scenario(
|
||||
&scenario,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
if save_scenario_task.await.is_ok() {
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self.custom_mode.read(cx).program.read(cx).is_empty(cx),
|
||||
),
|
||||
),
|
||||
this.save_scenario_state = Some(SaveScenarioState::Saving);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let res = save_scenario.await;
|
||||
|
||||
this.update(cx, |this, _| match res {
|
||||
Ok(saved_file) => {
|
||||
this.save_scenario_state =
|
||||
Some(SaveScenarioState::Saved(saved_file))
|
||||
}
|
||||
Err(error) => {
|
||||
this.save_scenario_state =
|
||||
Some(SaveScenarioState::Failed(
|
||||
error.to_string().into(),
|
||||
))
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_secs(2))
|
||||
.await;
|
||||
this.update(cx, |this, _| {
|
||||
this.save_scenario_state.take()
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.custom_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
.is_empty(cx)
|
||||
|| self.save_scenario_state.is_some(),
|
||||
),
|
||||
)
|
||||
.when_some(self.save_scenario_state.as_ref(), {
|
||||
let this_entity = this.clone();
|
||||
|
||||
move |this, save_state| match save_state {
|
||||
SaveScenarioState::Saved(saved_path) => this.child(
|
||||
IconButton::new(
|
||||
"new-session-modal-go-to-file",
|
||||
IconName::ArrowUpRight,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click({
|
||||
let this_entity = this_entity.clone();
|
||||
let saved_path = saved_path.clone();
|
||||
move |_, window, cx| {
|
||||
window
|
||||
.spawn(cx, {
|
||||
let this_entity = this_entity.clone();
|
||||
let saved_path = saved_path.clone();
|
||||
|
||||
async move |cx| {
|
||||
this_entity
|
||||
.update_in(
|
||||
cx,
|
||||
|this, window, cx| {
|
||||
this.workspace.update(
|
||||
cx,
|
||||
|workspace, cx| {
|
||||
workspace.open_path(
|
||||
saved_path
|
||||
.clone(),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)??
|
||||
.await?;
|
||||
|
||||
this_entity
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}),
|
||||
),
|
||||
SaveScenarioState::Saving => this.child(
|
||||
Icon::new(IconName::Spinner)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"Spinner",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
),
|
||||
SaveScenarioState::Failed(error_msg) => this.child(
|
||||
IconButton::new("Failed Scenario Saved", IconName::X)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Error)
|
||||
.tooltip(ui::Tooltip::text(error_msg.clone())),
|
||||
),
|
||||
}
|
||||
}),
|
||||
})
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
@@ -595,7 +714,7 @@ impl CustomMode {
|
||||
|
||||
let program = cx.new(|cx| Editor::single_line(window, cx));
|
||||
program.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("Program path", cx);
|
||||
this.set_placeholder_text("Run", cx);
|
||||
|
||||
if let Some(past_program) = past_program {
|
||||
this.set_text(past_program, window, cx);
|
||||
@@ -617,11 +736,29 @@ impl CustomMode {
|
||||
|
||||
pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
|
||||
let path = self.cwd.read(cx).text(cx);
|
||||
let command = self.program.read(cx).text(cx);
|
||||
let mut args = shlex::split(&command).into_iter().flatten().peekable();
|
||||
let mut env = FxHashMap::default();
|
||||
while args.peek().is_some_and(|arg| arg.contains('=')) {
|
||||
let arg = args.next().unwrap();
|
||||
let (lhs, rhs) = arg.split_once('=').unwrap();
|
||||
env.insert(lhs.to_string(), rhs.to_string());
|
||||
}
|
||||
|
||||
let program = if let Some(program) = args.next() {
|
||||
program
|
||||
} else {
|
||||
env = FxHashMap::default();
|
||||
command
|
||||
};
|
||||
|
||||
let args = args.collect::<Vec<_>>();
|
||||
|
||||
task::LaunchRequest {
|
||||
program: self.program.read(cx).text(cx),
|
||||
program,
|
||||
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
|
||||
args: Default::default(),
|
||||
env: Default::default(),
|
||||
args,
|
||||
env,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::{
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use dap::OutputEvent;
|
||||
use editor::{CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
Context, Entity, FocusHandle, Focusable, Render, Subscription, Task, TextStyle, WeakEntity,
|
||||
@@ -401,28 +401,21 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
.as_ref()
|
||||
.unwrap_or(&completion.label)
|
||||
.to_owned();
|
||||
let mut word_bytes_length = 0;
|
||||
for chunk in snapshot
|
||||
.reversed_chunks_in_range(language::Anchor::MIN..buffer_position)
|
||||
{
|
||||
let mut processed_bytes = 0;
|
||||
if let Some(_) = chunk.chars().rfind(|c| {
|
||||
let is_whitespace = c.is_whitespace();
|
||||
if !is_whitespace {
|
||||
processed_bytes += c.len_utf8();
|
||||
}
|
||||
let buffer_text = snapshot.text();
|
||||
let buffer_bytes = buffer_text.as_bytes();
|
||||
let new_bytes = new_text.as_bytes();
|
||||
|
||||
is_whitespace
|
||||
}) {
|
||||
word_bytes_length += processed_bytes;
|
||||
let mut prefix_len = 0;
|
||||
for i in (0..new_bytes.len()).rev() {
|
||||
if buffer_bytes.ends_with(&new_bytes[0..i]) {
|
||||
prefix_len = i;
|
||||
break;
|
||||
} else {
|
||||
word_bytes_length += chunk.len();
|
||||
}
|
||||
}
|
||||
|
||||
let buffer_offset = buffer_position.to_offset(&snapshot);
|
||||
let start = buffer_offset - word_bytes_length;
|
||||
let start = buffer_offset - prefix_len;
|
||||
let start = snapshot.clip_offset(start, Bias::Left);
|
||||
let start = snapshot.anchor_before(start);
|
||||
let replace_range = start..buffer_position;
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ use gpui::{
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use settings::Settings;
|
||||
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
|
||||
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
|
||||
|
||||
@@ -22,6 +24,11 @@ pub struct DiagnosticIndicator {
|
||||
|
||||
impl Render for DiagnosticIndicator {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let indicator = h_flex().gap_2();
|
||||
if !ProjectSettings::get_global(cx).diagnostics.button {
|
||||
return indicator;
|
||||
}
|
||||
|
||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_flex().map(|this| {
|
||||
this.child(
|
||||
@@ -84,8 +91,7 @@ impl Render for DiagnosticIndicator {
|
||||
None
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
indicator
|
||||
.child(
|
||||
ButtonLike::new("diagnostic-indicator")
|
||||
.child(diagnostic_indicator)
|
||||
|
||||
@@ -640,6 +640,7 @@ impl CompletionsMenu {
|
||||
MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
|
||||
@@ -8779,15 +8779,13 @@ impl Editor {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the selection is empty and the cursor is in the leading whitespace before the
|
||||
// suggested indentation, then auto-indent the line.
|
||||
let cursor = selection.head();
|
||||
let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row));
|
||||
if let Some(suggested_indent) =
|
||||
suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
|
||||
{
|
||||
// If there exist any empty selection in the leading whitespace, then skip
|
||||
// indent for selections at the boundary.
|
||||
// Don't do anything if already at suggested indent
|
||||
// and there is any other cursor which is not
|
||||
if has_some_cursor_in_whitespace
|
||||
&& cursor.column == current_indent.len
|
||||
&& current_indent.len == suggested_indent.len
|
||||
@@ -8795,6 +8793,8 @@ impl Editor {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust line and move cursor to suggested indent
|
||||
// if cursor is not at suggested indent
|
||||
if cursor.column < suggested_indent.len
|
||||
&& cursor.column <= current_indent.len
|
||||
&& current_indent.len <= suggested_indent.len
|
||||
@@ -8811,6 +8811,14 @@ impl Editor {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// If current indent is more than suggested indent
|
||||
// only move cursor to current indent and skip indent
|
||||
if cursor.column < current_indent.len && current_indent.len > suggested_indent.len {
|
||||
selection.start = Point::new(cursor.row, current_indent.len);
|
||||
selection.end = selection.start;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, insert a hard or soft tab.
|
||||
|
||||
@@ -4,6 +4,7 @@ use project::project_settings::DiagnosticSeverity;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, VsCodeSettings};
|
||||
use util::serde::default_true;
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct EditorSettings {
|
||||
@@ -276,6 +277,9 @@ pub enum ScrollBeyondLastLine {
|
||||
/// Default options for buffer and project search items.
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct SearchSettings {
|
||||
/// Whether to show the project search button in the status bar.
|
||||
#[serde(default = "default_true")]
|
||||
pub button: bool,
|
||||
#[serde(default)]
|
||||
pub whole_word: bool,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -2871,7 +2871,8 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
|
||||
);
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
|
||||
// when all cursors are to the left of the suggested indent, then auto-indent all.
|
||||
// test when all cursors are not at suggested indent
|
||||
// then simply move to their suggested indent location
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
@@ -2888,9 +2889,8 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
|
||||
);
|
||||
"});
|
||||
|
||||
// cursors that are already at the suggested indent level do not move
|
||||
// until other cursors that are to the left of the suggested indent
|
||||
// auto-indent.
|
||||
// test cursor already at suggested indent not moving when
|
||||
// other cursors are yet to reach their suggested indents
|
||||
cx.set_state(indoc! {"
|
||||
ˇ
|
||||
const a: B = (
|
||||
@@ -2914,8 +2914,7 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
// once all multi-cursors are at the suggested
|
||||
// indent level, they all insert a soft tab together.
|
||||
// test when all cursors are at suggested indent then tab is inserted
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
ˇ
|
||||
@@ -2929,6 +2928,112 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppConte
|
||||
);
|
||||
"});
|
||||
|
||||
// test when current indent is less than suggested indent,
|
||||
// we adjust line to match suggested indent and move cursor to it
|
||||
//
|
||||
// when no other cursor is at word boundary, all of them should move
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ )
|
||||
ˇ )
|
||||
);
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ)
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
|
||||
// test when current indent is less than suggested indent,
|
||||
// we adjust line to match suggested indent and move cursor to it
|
||||
//
|
||||
// when some other cursor is at word boundary, it should not move
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ )
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ)
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
|
||||
// test when current indent is more than suggested indent,
|
||||
// we just move cursor to current indent instead of suggested indent
|
||||
//
|
||||
// when no other cursor is at word boundary, all of them should move
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ )
|
||||
ˇ )
|
||||
);
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ)
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ)
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
|
||||
// test when current indent is more than suggested indent,
|
||||
// we just move cursor to current indent instead of suggested indent
|
||||
//
|
||||
// when some other cursor is at word boundary, it doesn't move
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ )
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
const a: B = (
|
||||
c(
|
||||
d(
|
||||
ˇ
|
||||
ˇ)
|
||||
ˇ)
|
||||
);
|
||||
"});
|
||||
|
||||
// handle auto-indent when there are multiple cursors on the same line
|
||||
cx.set_state(indoc! {"
|
||||
const a: B = (
|
||||
@@ -8900,6 +9005,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
},
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
Ok(Some(json!(null)))
|
||||
}
|
||||
@@ -16414,7 +16520,7 @@ async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
|
||||
assert_indent_guides(
|
||||
0..6,
|
||||
vec![
|
||||
indent_guide(buffer_id, 1, 6, 0),
|
||||
indent_guide(buffer_id, 1, 5, 0),
|
||||
indent_guide(buffer_id, 3, 4, 1),
|
||||
],
|
||||
None,
|
||||
@@ -19153,6 +19259,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex
|
||||
},
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
.unwrap();
|
||||
Ok(Some(json!(null)))
|
||||
}
|
||||
|
||||
@@ -2882,8 +2882,7 @@ impl EditorElement {
|
||||
text_x: Pixels,
|
||||
rows: &Range<DisplayRow>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
right_margin: Pixels,
|
||||
editor_margins: &EditorMargins,
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
text_hitbox: &Hitbox,
|
||||
@@ -2943,11 +2942,6 @@ impl EditorElement {
|
||||
})
|
||||
.is_ok();
|
||||
|
||||
let margins = EditorMargins {
|
||||
gutter: *gutter_dimensions,
|
||||
right: right_margin,
|
||||
};
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.children(
|
||||
@@ -2956,7 +2950,7 @@ impl EditorElement {
|
||||
window,
|
||||
app: cx,
|
||||
anchor_x,
|
||||
margins: &margins,
|
||||
margins: editor_margins,
|
||||
line_height,
|
||||
em_width,
|
||||
block_id,
|
||||
@@ -2975,7 +2969,7 @@ impl EditorElement {
|
||||
..
|
||||
} => {
|
||||
let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id);
|
||||
let result = v_flex().id(block_id).w_full();
|
||||
let result = v_flex().id(block_id).w_full().pr(editor_margins.right);
|
||||
|
||||
let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt);
|
||||
result
|
||||
@@ -3006,8 +3000,10 @@ impl EditorElement {
|
||||
if sticky_header_excerpt_id != Some(excerpt.id) {
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
|
||||
result = result.child(self.render_buffer_header(
|
||||
excerpt, false, selected, false, jump_data, window, cx,
|
||||
result = result.child(div().pr(editor_margins.right).child(
|
||||
self.render_buffer_header(
|
||||
excerpt, false, selected, false, jump_data, window, cx,
|
||||
),
|
||||
));
|
||||
} else {
|
||||
result =
|
||||
@@ -3054,7 +3050,7 @@ impl EditorElement {
|
||||
if let Some((x_target, line_width)) = x_position {
|
||||
let margin = em_width * 2;
|
||||
if line_width + final_size.width + margin
|
||||
< editor_width + gutter_dimensions.full_width()
|
||||
< editor_width + editor_margins.gutter.full_width()
|
||||
&& !row_block_types.contains_key(&(row - 1))
|
||||
&& element_height_in_lines == 1
|
||||
{
|
||||
@@ -3064,10 +3060,10 @@ impl EditorElement {
|
||||
element_height_in_lines = 0;
|
||||
row_block_types.insert(row, is_block);
|
||||
} else {
|
||||
let max_offset =
|
||||
editor_width + gutter_dimensions.full_width() - final_size.width;
|
||||
let max_offset = editor_width + editor_margins.gutter.full_width()
|
||||
- final_size.width;
|
||||
let min_offset = (x_target + em_width - final_size.width)
|
||||
.max(gutter_dimensions.full_width());
|
||||
.max(editor_margins.gutter.full_width());
|
||||
x_offset = x_target.min(max_offset).max(min_offset);
|
||||
}
|
||||
}
|
||||
@@ -3283,8 +3279,7 @@ impl EditorElement {
|
||||
text_hitbox: &Hitbox,
|
||||
editor_width: Pixels,
|
||||
scroll_width: &mut Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
right_margin: Pixels,
|
||||
editor_margins: &EditorMargins,
|
||||
em_width: Pixels,
|
||||
text_x: Pixels,
|
||||
line_height: Pixels,
|
||||
@@ -3324,8 +3319,7 @@ impl EditorElement {
|
||||
text_x,
|
||||
&rows,
|
||||
line_layouts,
|
||||
gutter_dimensions,
|
||||
right_margin,
|
||||
editor_margins,
|
||||
line_height,
|
||||
em_width,
|
||||
text_hitbox,
|
||||
@@ -3363,7 +3357,7 @@ impl EditorElement {
|
||||
.size
|
||||
.width
|
||||
.max(fixed_block_max_width)
|
||||
.max(gutter_dimensions.width + *scroll_width)
|
||||
.max(editor_margins.gutter.width + *scroll_width)
|
||||
.into(),
|
||||
(BlockStyle::Fixed, _) => unreachable!(),
|
||||
};
|
||||
@@ -3382,8 +3376,7 @@ impl EditorElement {
|
||||
text_x,
|
||||
&rows,
|
||||
line_layouts,
|
||||
gutter_dimensions,
|
||||
right_margin,
|
||||
editor_margins,
|
||||
line_height,
|
||||
em_width,
|
||||
text_hitbox,
|
||||
@@ -3423,7 +3416,7 @@ impl EditorElement {
|
||||
.size
|
||||
.width
|
||||
.max(fixed_block_max_width)
|
||||
.max(gutter_dimensions.width + *scroll_width),
|
||||
.max(editor_margins.gutter.width + *scroll_width),
|
||||
),
|
||||
BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
|
||||
};
|
||||
@@ -3437,8 +3430,7 @@ impl EditorElement {
|
||||
text_x,
|
||||
&rows,
|
||||
line_layouts,
|
||||
gutter_dimensions,
|
||||
right_margin,
|
||||
editor_margins,
|
||||
line_height,
|
||||
em_width,
|
||||
text_hitbox,
|
||||
@@ -3470,7 +3462,8 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if resized_blocks.is_empty() {
|
||||
*scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width);
|
||||
*scroll_width =
|
||||
(*scroll_width).max(fixed_block_max_width - editor_margins.gutter.width);
|
||||
Ok((blocks, row_block_types))
|
||||
} else {
|
||||
Err(resized_blocks)
|
||||
@@ -3523,6 +3516,7 @@ impl EditorElement {
|
||||
StickyHeaderExcerpt { excerpt }: StickyHeaderExcerpt<'_>,
|
||||
scroll_position: f32,
|
||||
line_height: Pixels,
|
||||
right_margin: Pixels,
|
||||
snapshot: &EditorSnapshot,
|
||||
hitbox: &Hitbox,
|
||||
selected_buffer_ids: &Vec<BufferId>,
|
||||
@@ -3541,11 +3535,13 @@ impl EditorElement {
|
||||
|
||||
let selected = selected_buffer_ids.contains(&excerpt.buffer_id);
|
||||
|
||||
let available_width = hitbox.bounds.size.width - right_margin;
|
||||
|
||||
let mut header = v_flex()
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.w(hitbox.bounds.size.width)
|
||||
.w(available_width)
|
||||
.h(FILE_HEADER_HEIGHT as f32 * line_height)
|
||||
.bg(linear_gradient(
|
||||
0.,
|
||||
@@ -3582,7 +3578,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
let size = size(
|
||||
AvailableSpace::Definite(hitbox.size.width),
|
||||
AvailableSpace::Definite(available_width),
|
||||
AvailableSpace::MinContent,
|
||||
);
|
||||
|
||||
@@ -7177,9 +7173,14 @@ impl Element for EditorElement {
|
||||
let editor_width =
|
||||
text_width - gutter_dimensions.margin - 2 * em_width - right_margin;
|
||||
|
||||
let editor_margins = EditorMargins {
|
||||
gutter: gutter_dimensions,
|
||||
right: right_margin,
|
||||
};
|
||||
|
||||
// Offset the content_bounds from the text_bounds by the gutter margin (which
|
||||
// is roughly half a character wide) to make hit testing work more like how we want.
|
||||
let content_offset = point(gutter_dimensions.margin, Pixels::ZERO);
|
||||
let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO);
|
||||
|
||||
let editor_content_width = editor_width - content_offset.x;
|
||||
|
||||
@@ -7635,8 +7636,7 @@ impl Element for EditorElement {
|
||||
&text_hitbox,
|
||||
editor_width,
|
||||
&mut scroll_width,
|
||||
&gutter_dimensions,
|
||||
right_margin,
|
||||
&editor_margins,
|
||||
em_width,
|
||||
gutter_dimensions.full_width(),
|
||||
line_height,
|
||||
@@ -7665,6 +7665,7 @@ impl Element for EditorElement {
|
||||
sticky_header_excerpt,
|
||||
scroll_position.y,
|
||||
line_height,
|
||||
right_margin,
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&selected_buffer_ids,
|
||||
|
||||
@@ -897,6 +897,7 @@ impl InfoPopover {
|
||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
copy_button_on_hover: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
|
||||
@@ -1409,6 +1409,7 @@ pub mod tests {
|
||||
fake_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.into_response()
|
||||
.expect("inlay refresh request failed");
|
||||
cx.executor().run_until_parked();
|
||||
editor
|
||||
@@ -1492,6 +1493,7 @@ pub mod tests {
|
||||
token: lsp::ProgressToken::String(progress_token.to_string()),
|
||||
})
|
||||
.await
|
||||
.into_response()
|
||||
.expect("work done progress create request failed");
|
||||
cx.executor().run_until_parked();
|
||||
fake_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams {
|
||||
@@ -1863,6 +1865,7 @@ pub mod tests {
|
||||
fake_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.into_response()
|
||||
.expect("inlay refresh request failed");
|
||||
cx.executor().run_until_parked();
|
||||
editor
|
||||
@@ -2008,6 +2011,7 @@ pub mod tests {
|
||||
fake_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.into_response()
|
||||
.expect("inlay refresh request failed");
|
||||
cx.executor().run_until_parked();
|
||||
editor
|
||||
@@ -2070,6 +2074,7 @@ pub mod tests {
|
||||
fake_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.into_response()
|
||||
.expect("inlay refresh request failed");
|
||||
cx.executor().run_until_parked();
|
||||
editor
|
||||
|
||||
@@ -397,7 +397,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
cx.observe_global::<SettingsStore>(move |cx| {
|
||||
let settings = &ProjectSettings::get_global(cx).node;
|
||||
let options = NodeBinaryOptions {
|
||||
allow_path_lookup: !settings.ignore_system_version.unwrap_or_default(),
|
||||
allow_path_lookup: !settings.ignore_system_version,
|
||||
allow_binary_download: true,
|
||||
use_paths: settings.path.as_ref().map(|node_path| {
|
||||
let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
|
||||
@@ -417,14 +417,14 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
tx.send(Some(options)).log_err();
|
||||
})
|
||||
.detach();
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), rx);
|
||||
let node_runtime = NodeRuntime::new(client.http_client(), None, rx);
|
||||
|
||||
let extension_host_proxy = ExtensionHostProxy::global(cx);
|
||||
|
||||
language::init(cx);
|
||||
language_extension::init(extension_host_proxy.clone(), languages.clone());
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
language_models::init(user_store.clone(), client.clone(), cx);
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
prompt_store::init(cx);
|
||||
let stdout_is_a_pty = false;
|
||||
|
||||
19
crates/eval/src/examples/no_tools_enabled.toml
Normal file
19
crates/eval/src/examples/no_tools_enabled.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
url = "https://github.com/zed-industries/zed"
|
||||
revision = "main"
|
||||
require_lsp = false
|
||||
prompt = """
|
||||
I need to explore the codebase to understand what files are available in the project. What can you tell me about the structure of the codebase?
|
||||
|
||||
Please find all uses of the 'find_path' function in the src directory.
|
||||
|
||||
Also, can you tell me what the capital of France is? And how does garbage collection work in programming languages?
|
||||
"""
|
||||
|
||||
profile_name = "minimal"
|
||||
|
||||
[thread_assertions]
|
||||
no_hallucinated_tool_calls = """The agent should not hallucinate tool calls - for example, by writing markdown code blocks that simulate commands like `find`, `grep`, `ls`, etc. - since no tools are available. However, it is totally fine if the agent describes to the user what should be done, e.g. telling the user \"You can run `find` to...\" etc."""
|
||||
|
||||
doesnt_hallucinate_file_paths = """The agent should not make up file paths or pretend to know the structure of the project when tools are not available."""
|
||||
|
||||
correctly_answers_general_questions = """The agent should correctly answer general knowledge questions about the capital of France and garbage collection without asking for more context, demonstrating it can still be helpful with areas it knows about."""
|
||||
@@ -806,6 +806,6 @@ trait ToWasmtimeResult<T> {
|
||||
|
||||
impl<T> ToWasmtimeResult<T> for Result<T> {
|
||||
fn to_wasmtime_result(self) -> wasmtime::Result<Result<T, String>> {
|
||||
Ok(self.map_err(|error| error.to_string()))
|
||||
Ok(self.map_err(|error| format!("{error:?}")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2340,15 +2340,19 @@ impl Fs for FakeFs {
|
||||
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
||||
rope.chunks().flat_map(move |chunk| {
|
||||
let mut newline = false;
|
||||
chunk.split('\n').flat_map(move |line| {
|
||||
let ending = if newline {
|
||||
Some(line_ending.as_str())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
newline = true;
|
||||
ending.into_iter().chain([line])
|
||||
})
|
||||
let end_with_newline = chunk.ends_with('\n').then_some(line_ending.as_str());
|
||||
chunk
|
||||
.lines()
|
||||
.flat_map(move |line| {
|
||||
let ending = if newline {
|
||||
Some(line_ending.as_str())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
newline = true;
|
||||
ending.into_iter().chain([line])
|
||||
})
|
||||
.chain(end_with_newline)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,6 @@ impl<'a> Matcher<'a> {
|
||||
if score <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let path_len = prefix.len() + path.len();
|
||||
let mut cur_start = 0;
|
||||
let mut byte_ix = 0;
|
||||
@@ -173,8 +172,17 @@ impl<'a> Matcher<'a> {
|
||||
byte_ix += ch.len_utf8();
|
||||
char_ix += 1;
|
||||
}
|
||||
cur_start = match_char_ix + 1;
|
||||
|
||||
self.match_positions[i] = byte_ix;
|
||||
|
||||
let matched_ch = prefix
|
||||
.get(match_char_ix)
|
||||
.or_else(|| path.get(match_char_ix - prefix.len()))
|
||||
.unwrap();
|
||||
byte_ix += matched_ch.len_utf8();
|
||||
|
||||
cur_start = match_char_ix + 1;
|
||||
char_ix = match_char_ix + 1;
|
||||
}
|
||||
|
||||
score
|
||||
@@ -209,8 +217,11 @@ impl<'a> Matcher<'a> {
|
||||
let query_char = self.lowercase_query[query_idx];
|
||||
let limit = self.last_positions[query_idx];
|
||||
|
||||
let max_valid_index = (prefix.len() + path_lowercased.len()).saturating_sub(1);
|
||||
let safe_limit = limit.min(max_valid_index);
|
||||
|
||||
let mut last_slash = 0;
|
||||
for j in path_idx..=limit {
|
||||
for j in path_idx..=safe_limit {
|
||||
let extra_lowercase_chars_count = extra_lowercase_chars
|
||||
.iter()
|
||||
.take_while(|(i, _)| i < &&j)
|
||||
@@ -218,10 +229,15 @@ impl<'a> Matcher<'a> {
|
||||
.sum::<usize>();
|
||||
let j_regular = j - extra_lowercase_chars_count;
|
||||
|
||||
let path_char = if j_regular < prefix.len() {
|
||||
let path_char = if j < prefix.len() {
|
||||
lowercase_prefix[j]
|
||||
} else {
|
||||
path_lowercased[j - prefix.len()]
|
||||
let path_index = j - prefix.len();
|
||||
if path_index < path_lowercased.len() {
|
||||
path_lowercased[path_index]
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let is_path_sep = path_char == MAIN_SEPARATOR;
|
||||
|
||||
@@ -490,6 +506,89 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_unicode_path_entries() {
|
||||
let mixed_unicode_paths = vec![
|
||||
"İolu/oluş",
|
||||
"İstanbul/code",
|
||||
"Athens/Şanlıurfa",
|
||||
"Çanakkale/scripts",
|
||||
"paris/Düzce_İl",
|
||||
"Berlin_Önemli_Ğündem",
|
||||
"KİTAPLIK/london/dosya",
|
||||
"tokyo/kyoto/fuji",
|
||||
"new_york/san_francisco",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("İo/oluş", false, &mixed_unicode_paths),
|
||||
vec![("İolu/oluş", vec![0, 2, 4, 6, 8, 10, 12])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("İst/code", false, &mixed_unicode_paths),
|
||||
vec![("İstanbul/code", vec![0, 2, 4, 6, 8, 10, 12, 14])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("athens/şa", false, &mixed_unicode_paths),
|
||||
vec![("Athens/Şanlıurfa", vec![0, 1, 2, 3, 4, 5, 6, 7, 9])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("BerlinÖĞ", false, &mixed_unicode_paths),
|
||||
vec![("Berlin_Önemli_Ğündem", vec![0, 1, 2, 3, 4, 5, 7, 15])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("tokyo/fuji", false, &mixed_unicode_paths),
|
||||
vec![("tokyo/kyoto/fuji", vec![0, 1, 2, 3, 4, 5, 12, 13, 14, 15])]
|
||||
);
|
||||
|
||||
let mixed_script_paths = vec![
|
||||
"résumé_Москва",
|
||||
"naïve_київ_implementation",
|
||||
"café_北京_app",
|
||||
"東京_über_driver",
|
||||
"déjà_vu_cairo",
|
||||
"seoul_piñata_game",
|
||||
"voilà_istanbul_result",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("résmé", false, &mixed_script_paths),
|
||||
vec![("résumé_Москва", vec![0, 1, 3, 5, 6])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("café北京", false, &mixed_script_paths),
|
||||
vec![("café_北京_app", vec![0, 1, 2, 3, 6, 9])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("ista", false, &mixed_script_paths),
|
||||
vec![("voilà_istanbul_result", vec![7, 8, 9, 10])]
|
||||
);
|
||||
|
||||
let complex_paths = vec![
|
||||
"document_📚_library",
|
||||
"project_👨👩👧👦_family",
|
||||
"flags_🇯🇵🇺🇸🇪🇺_world",
|
||||
"code_😀😃😄😁_happy",
|
||||
"photo_👩👩👧👦_album",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("doc📚lib", false, &complex_paths),
|
||||
vec![("document_📚_library", vec![0, 1, 2, 9, 14, 15, 16])]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
match_single_path_query("codehappy", false, &complex_paths),
|
||||
vec![("code_😀😃😄😁_happy", vec![0, 1, 2, 3, 22, 23, 24, 25, 26])]
|
||||
);
|
||||
}
|
||||
|
||||
fn match_single_path_query<'a>(
|
||||
query: &str,
|
||||
smart_case: bool,
|
||||
|
||||
@@ -306,8 +306,7 @@ impl PickerDelegate for BranchListDelegate {
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await
|
||||
.iter()
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.map(|candidate| BranchEntry {
|
||||
branch: all_branches[candidate.candidate_id].clone(),
|
||||
positions: candidate.positions,
|
||||
|
||||
@@ -1051,8 +1051,8 @@ impl GitPanel {
|
||||
repo.checkout_files(
|
||||
"HEAD",
|
||||
entries
|
||||
.iter()
|
||||
.map(|entries| entries.repo_path.clone())
|
||||
.into_iter()
|
||||
.map(|entries| entries.repo_path)
|
||||
.collect(),
|
||||
cx,
|
||||
)
|
||||
@@ -2765,9 +2765,9 @@ impl GitPanel {
|
||||
let potential_co_authors = self.potential_co_authors(cx);
|
||||
|
||||
let (tooltip_label, icon) = if self.add_coauthors {
|
||||
("Add co-authored-by", IconName::UserCheck)
|
||||
} else {
|
||||
("Remove co-authored-by", IconName::Person)
|
||||
} else {
|
||||
("Add co-authored-by", IconName::UserCheck)
|
||||
};
|
||||
|
||||
if potential_co_authors.is_empty() {
|
||||
|
||||
@@ -161,7 +161,7 @@ blade-graphics = { workspace = true, optional = true }
|
||||
blade-macros = { workspace = true, optional = true }
|
||||
blade-util = { workspace = true, optional = true }
|
||||
bytemuck = { version = "1", optional = true }
|
||||
cosmic-text = { version = "0.13.2", optional = true }
|
||||
cosmic-text = { version = "0.14.0", optional = true }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5474cfad4b719a72ec8ed2cb7327b2b01fd10568", features = [
|
||||
"source-fontconfig-dlopen",
|
||||
], optional = true }
|
||||
|
||||
@@ -490,7 +490,7 @@ impl Interactivity {
|
||||
|
||||
/// Bind the given callback on the hover start and end events of this element. Note that the boolean
|
||||
/// passed to the callback is true when the hover starts and false when it ends.
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_drag`]
|
||||
/// The imperative API equivalent to [`StatefulInteractiveElement::on_hover`]
|
||||
///
|
||||
/// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback.
|
||||
pub fn on_hover(&mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static)
|
||||
@@ -544,6 +544,15 @@ impl Interactivity {
|
||||
pub fn occlude_mouse(&mut self) {
|
||||
self.occlude_mouse = true;
|
||||
}
|
||||
|
||||
/// Registers event handles that stop propagation of mouse events for non-scroll events.
|
||||
/// The imperative API equivalent to [`InteractiveElement::block_mouse_except_scroll`]
|
||||
pub fn stop_mouse_events_except_scroll(&mut self) {
|
||||
self.on_any_mouse_down(|_, _, cx| cx.stop_propagation());
|
||||
self.on_any_mouse_up(|_, _, cx| cx.stop_propagation());
|
||||
self.on_click(|_, _, cx| cx.stop_propagation());
|
||||
self.on_hover(|_, _, cx| cx.stop_propagation());
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for elements that want to use the standard GPUI event handlers that don't
|
||||
@@ -919,11 +928,17 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Block the mouse from interacting with this element or any of its children
|
||||
/// The fluent API equivalent to [`Interactivity::occlude_mouse`]
|
||||
/// Stops propagation of left mouse down event.
|
||||
fn block_mouse_down(mut self) -> Self {
|
||||
self.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
}
|
||||
|
||||
/// Registers event handles that stop propagation of mouse events for non-scroll events.
|
||||
/// The fluent API equivalent to [`Interactivity::block_mouse_except_scroll`]
|
||||
fn stop_mouse_events_except_scroll(mut self) -> Self {
|
||||
self.interactivity().stop_mouse_events_except_scroll();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for elements that want to use the standard GPUI interactivity features
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::{
|
||||
use anyhow::{Context as _, Ok, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use cosmic_text::{
|
||||
Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontSystem, ShapeBuffer, ShapeLine,
|
||||
SwashCache,
|
||||
Attrs, AttrsList, CacheKey, Family, Font as CosmicTextFont, FontFeatures as CosmicFontFeatures,
|
||||
FontSystem, ShapeBuffer, ShapeLine, SwashCache,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
@@ -21,15 +21,29 @@ use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
pub(crate) struct CosmicTextSystem(RwLock<CosmicTextSystemState>);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct FontKey {
|
||||
family: SharedString,
|
||||
features: FontFeatures,
|
||||
}
|
||||
|
||||
impl FontKey {
|
||||
fn new(family: SharedString, features: FontFeatures) -> Self {
|
||||
Self { family, features }
|
||||
}
|
||||
}
|
||||
|
||||
struct CosmicTextSystemState {
|
||||
swash_cache: SwashCache,
|
||||
font_system: FontSystem,
|
||||
scratch: ShapeBuffer,
|
||||
/// Contains all already loaded fonts, including all faces. Indexed by `FontId`.
|
||||
loaded_fonts_store: Vec<Arc<CosmicTextFont>>,
|
||||
/// Contains enabled font features for each loaded font.
|
||||
features_store: Vec<CosmicFontFeatures>,
|
||||
/// Caches the `FontId`s associated with a specific family to avoid iterating the font database
|
||||
/// for every font face in a family.
|
||||
font_ids_by_family_cache: HashMap<SharedString, SmallVec<[FontId; 4]>>,
|
||||
font_ids_by_family_cache: HashMap<FontKey, SmallVec<[FontId; 4]>>,
|
||||
/// The name of each font associated with the given font id
|
||||
postscript_names: HashMap<FontId, String>,
|
||||
}
|
||||
@@ -44,6 +58,7 @@ impl CosmicTextSystem {
|
||||
swash_cache: SwashCache::new(),
|
||||
scratch: ShapeBuffer::default(),
|
||||
loaded_fonts_store: Vec::new(),
|
||||
features_store: Vec::new(),
|
||||
font_ids_by_family_cache: HashMap::default(),
|
||||
postscript_names: HashMap::default(),
|
||||
}))
|
||||
@@ -78,15 +93,13 @@ impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn font_id(&self, font: &Font) -> Result<FontId> {
|
||||
// todo(linux): Do we need to use CosmicText's Font APIs? Can we consolidate this to use font_kit?
|
||||
let mut state = self.0.write();
|
||||
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&font.family) {
|
||||
let key = FontKey::new(font.family.clone(), font.features.clone());
|
||||
let candidates = if let Some(font_ids) = state.font_ids_by_family_cache.get(&key) {
|
||||
font_ids.as_slice()
|
||||
} else {
|
||||
let font_ids = state.load_family(&font.family, &font.features)?;
|
||||
state
|
||||
.font_ids_by_family_cache
|
||||
.insert(font.family.clone(), font_ids);
|
||||
state.font_ids_by_family_cache[&font.family].as_ref()
|
||||
state.font_ids_by_family_cache.insert(key.clone(), font_ids);
|
||||
state.font_ids_by_family_cache[&key].as_ref()
|
||||
};
|
||||
|
||||
// todo(linux) ideally we would make fontdb's `find_best_match` pub instead of using font-kit here
|
||||
@@ -229,9 +242,24 @@ impl CosmicTextSystemState {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Convert features into cosmic_text struct.
|
||||
let mut font_features = CosmicFontFeatures::new();
|
||||
for feature in _features.0.iter() {
|
||||
let name_bytes: [u8; 4] = feature
|
||||
.0
|
||||
.as_bytes()
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("Incorrect feature flag format"))?;
|
||||
|
||||
let tag = cosmic_text::FeatureTag::new(&name_bytes);
|
||||
|
||||
font_features.set(tag, feature.1);
|
||||
}
|
||||
|
||||
let font_id = FontId(self.loaded_fonts_store.len());
|
||||
font_ids.push(font_id);
|
||||
self.loaded_fonts_store.push(font);
|
||||
self.features_store.push(font_features);
|
||||
self.postscript_names.insert(font_id, postscript_name);
|
||||
}
|
||||
|
||||
@@ -271,6 +299,9 @@ impl CosmicTextSystemState {
|
||||
|
||||
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let font = &self.loaded_fonts_store[params.font_id.0];
|
||||
let subpixel_shift = params
|
||||
.subpixel_variant
|
||||
.map(|v| v as f32 / (SUBPIXEL_VARIANTS as f32 * params.scale_factor));
|
||||
let image = self
|
||||
.swash_cache
|
||||
.get_image(
|
||||
@@ -279,7 +310,7 @@ impl CosmicTextSystemState {
|
||||
font.id(),
|
||||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(0.0, 0.0),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
@@ -361,18 +392,22 @@ impl CosmicTextSystemState {
|
||||
|
||||
#[profiling::function]
|
||||
fn layout_line(&mut self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
|
||||
let mut attrs_list = AttrsList::new(Attrs::new());
|
||||
let mut attrs_list = AttrsList::new(&Attrs::new());
|
||||
let mut offs = 0;
|
||||
for run in font_runs {
|
||||
let font = &self.loaded_fonts_store[run.font_id.0];
|
||||
let font = self.font_system.db().face(font.id()).unwrap();
|
||||
|
||||
let features = self.features_store[run.font_id.0].clone();
|
||||
|
||||
attrs_list.add_span(
|
||||
offs..(offs + run.len),
|
||||
Attrs::new()
|
||||
&Attrs::new()
|
||||
.family(Family::Name(&font.families.first().unwrap().0))
|
||||
.stretch(font.stretch)
|
||||
.style(font.style)
|
||||
.weight(font.weight),
|
||||
.weight(font.weight)
|
||||
.font_features(features),
|
||||
);
|
||||
offs += run.len;
|
||||
}
|
||||
|
||||
@@ -5,23 +5,21 @@ use collections::{FxHashMap, FxHashSet};
|
||||
use itertools::Itertools;
|
||||
use util::ResultExt;
|
||||
use windows::Win32::{
|
||||
Foundation::HANDLE,
|
||||
Foundation::{HANDLE, HGLOBAL},
|
||||
System::{
|
||||
DataExchange::{
|
||||
CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats,
|
||||
GetClipboardData, GetClipboardFormatNameW, IsClipboardFormatAvailable, OpenClipboard,
|
||||
RegisterClipboardFormatW, SetClipboardData,
|
||||
},
|
||||
Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalUnlock},
|
||||
Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock},
|
||||
Ole::{CF_HDROP, CF_UNICODETEXT},
|
||||
},
|
||||
UI::Shell::{DragQueryFileW, HDROP},
|
||||
};
|
||||
use windows_core::PCWSTR;
|
||||
|
||||
use crate::{
|
||||
ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, SmartGlobal, hash,
|
||||
};
|
||||
use crate::{ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, hash};
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
@@ -268,13 +266,10 @@ where
|
||||
}
|
||||
|
||||
fn read_string_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let text = {
|
||||
let global = SmartGlobal::from_raw_ptr(
|
||||
unsafe { GetClipboardData(CF_UNICODETEXT.0 as u32).log_err() }?.0,
|
||||
);
|
||||
let text = PCWSTR(global.lock() as *const u16);
|
||||
String::from_utf16_lossy(unsafe { text.as_wide() })
|
||||
};
|
||||
let text = with_clipboard_data(CF_UNICODETEXT.0 as u32, |data_ptr| {
|
||||
let pcwstr = PCWSTR(data_ptr as *const u16);
|
||||
String::from_utf16_lossy(unsafe { pcwstr.as_wide() })
|
||||
})?;
|
||||
let Some(hash) = read_hash_from_clipboard() else {
|
||||
return Some(ClipboardEntry::String(ClipboardString::new(text)));
|
||||
};
|
||||
@@ -295,25 +290,23 @@ fn read_hash_from_clipboard() -> Option<u64> {
|
||||
if unsafe { IsClipboardFormatAvailable(*CLIPBOARD_HASH_FORMAT).is_err() } {
|
||||
return None;
|
||||
}
|
||||
let global =
|
||||
SmartGlobal::from_raw_ptr(unsafe { GetClipboardData(*CLIPBOARD_HASH_FORMAT).log_err() }?.0);
|
||||
let raw_ptr = global.lock() as *const u16;
|
||||
let hash_bytes: [u8; 8] = unsafe {
|
||||
std::slice::from_raw_parts(raw_ptr.cast::<u8>(), 8)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.log_err()
|
||||
}?;
|
||||
Some(u64::from_ne_bytes(hash_bytes))
|
||||
with_clipboard_data(*CLIPBOARD_HASH_FORMAT, |data_ptr| {
|
||||
let hash_bytes: [u8; 8] = unsafe {
|
||||
std::slice::from_raw_parts(data_ptr.cast::<u8>(), 8)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.log_err()
|
||||
}?;
|
||||
Some(u64::from_ne_bytes(hash_bytes))
|
||||
})?
|
||||
}
|
||||
|
||||
fn read_metadata_from_clipboard() -> Option<String> {
|
||||
unsafe { IsClipboardFormatAvailable(*CLIPBOARD_METADATA_FORMAT).log_err()? };
|
||||
let global = SmartGlobal::from_raw_ptr(
|
||||
unsafe { GetClipboardData(*CLIPBOARD_METADATA_FORMAT).log_err() }?.0,
|
||||
);
|
||||
let text = PCWSTR(global.lock() as *const u16);
|
||||
Some(String::from_utf16_lossy(unsafe { text.as_wide() }))
|
||||
with_clipboard_data(*CLIPBOARD_METADATA_FORMAT, |data_ptr| {
|
||||
let pcwstr = PCWSTR(data_ptr as *const u16);
|
||||
String::from_utf16_lossy(unsafe { pcwstr.as_wide() })
|
||||
})
|
||||
}
|
||||
|
||||
fn read_image_from_clipboard(format: u32) -> Option<ClipboardEntry> {
|
||||
@@ -327,29 +320,52 @@ fn format_number_to_image_format(format_number: u32) -> Option<&'static ImageFor
|
||||
}
|
||||
|
||||
fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option<ClipboardEntry> {
|
||||
let global = SmartGlobal::from_raw_ptr(unsafe { GetClipboardData(format_number).log_err() }?.0);
|
||||
let image_ptr = global.lock();
|
||||
let iamge_size = global.size();
|
||||
let bytes =
|
||||
unsafe { std::slice::from_raw_parts(image_ptr as *mut u8 as _, iamge_size).to_vec() };
|
||||
let id = hash(&bytes);
|
||||
let (bytes, id) = with_clipboard_data_and_size(format_number, |data_ptr, size| {
|
||||
let bytes = unsafe { std::slice::from_raw_parts(data_ptr as *mut u8 as _, size).to_vec() };
|
||||
let id = hash(&bytes);
|
||||
(bytes, id)
|
||||
})?;
|
||||
Some(ClipboardEntry::Image(Image { format, bytes, id }))
|
||||
}
|
||||
|
||||
fn read_files_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let global =
|
||||
SmartGlobal::from_raw_ptr(unsafe { GetClipboardData(CF_HDROP.0 as u32).log_err() }?.0);
|
||||
let hdrop = HDROP(global.lock());
|
||||
let mut filenames = String::new();
|
||||
with_file_names(hdrop, |file_name| {
|
||||
filenames.push_str(&file_name);
|
||||
});
|
||||
let text = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr| {
|
||||
let hdrop = HDROP(data_ptr);
|
||||
let mut filenames = String::new();
|
||||
with_file_names(hdrop, |file_name| {
|
||||
filenames.push_str(&file_name);
|
||||
});
|
||||
filenames
|
||||
})?;
|
||||
Some(ClipboardEntry::String(ClipboardString {
|
||||
text: filenames,
|
||||
text,
|
||||
metadata: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn with_clipboard_data<F, R>(format: u32, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(*mut std::ffi::c_void) -> R,
|
||||
{
|
||||
let global = HGLOBAL(unsafe { GetClipboardData(format).log_err() }?.0);
|
||||
let data_ptr = unsafe { GlobalLock(global) };
|
||||
let result = f(data_ptr);
|
||||
unsafe { GlobalUnlock(global).log_err() };
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn with_clipboard_data_and_size<F, R>(format: u32, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(*mut std::ffi::c_void, usize) -> R,
|
||||
{
|
||||
let global = HGLOBAL(unsafe { GetClipboardData(format).log_err() }?.0);
|
||||
let size = unsafe { GlobalSize(global) };
|
||||
let data_ptr = unsafe { GlobalLock(global) };
|
||||
let result = f(data_ptr, size);
|
||||
unsafe { GlobalUnlock(global).log_err() };
|
||||
Some(result)
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for image::ImageFormat {
|
||||
fn from(value: ImageFormat) -> Self {
|
||||
match value {
|
||||
|
||||
@@ -385,10 +385,6 @@ fn handle_keydown_msg(
|
||||
return Some(1);
|
||||
};
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
let event = match keystroke_or_modifier {
|
||||
KeystrokeOrModifier::Keystroke(keystroke) => PlatformInput::KeyDown(KeyDownEvent {
|
||||
@@ -396,9 +392,19 @@ fn handle_keydown_msg(
|
||||
is_held: lparam.0 & (0x1 << 30) > 0,
|
||||
}),
|
||||
KeystrokeOrModifier::Modifier(modifiers) => {
|
||||
if let Some(prev_modifiers) = lock.last_reported_modifiers {
|
||||
if prev_modifiers == modifiers {
|
||||
return Some(0);
|
||||
}
|
||||
}
|
||||
lock.last_reported_modifiers = Some(modifiers);
|
||||
PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers })
|
||||
}
|
||||
};
|
||||
let Some(mut func) = lock.callbacks.input.take() else {
|
||||
return Some(1);
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
let result = if func(event).default_prevented {
|
||||
Some(0)
|
||||
|
||||
@@ -42,6 +42,7 @@ pub struct WindowsWindowState {
|
||||
|
||||
pub callbacks: Callbacks,
|
||||
pub input_handler: Option<PlatformInputHandler>,
|
||||
pub last_reported_modifiers: Option<Modifiers>,
|
||||
pub system_key_handled: bool,
|
||||
pub hovered: bool,
|
||||
|
||||
@@ -100,6 +101,7 @@ impl WindowsWindowState {
|
||||
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
|
||||
let callbacks = Callbacks::default();
|
||||
let input_handler = None;
|
||||
let last_reported_modifiers = None;
|
||||
let system_key_handled = false;
|
||||
let hovered = false;
|
||||
let click_state = ClickState::new();
|
||||
@@ -118,6 +120,7 @@ impl WindowsWindowState {
|
||||
min_size,
|
||||
callbacks,
|
||||
input_handler,
|
||||
last_reported_modifiers,
|
||||
system_key_handled,
|
||||
hovered,
|
||||
renderer,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use util::ResultExt;
|
||||
use windows::Win32::{
|
||||
Foundation::{HANDLE, HGLOBAL},
|
||||
System::Memory::{GlobalLock, GlobalSize, GlobalUnlock},
|
||||
UI::WindowsAndMessaging::HCURSOR,
|
||||
};
|
||||
use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SafeHandle {
|
||||
@@ -50,30 +45,3 @@ impl Deref for SafeCursor {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SmartGlobal {
|
||||
raw: HGLOBAL,
|
||||
}
|
||||
|
||||
impl SmartGlobal {
|
||||
pub(crate) fn from_raw_ptr(ptr: *mut std::ffi::c_void) -> Self {
|
||||
Self { raw: HGLOBAL(ptr) }
|
||||
}
|
||||
|
||||
pub(crate) fn lock(&self) -> *mut std::ffi::c_void {
|
||||
unsafe { GlobalLock(self.raw) }
|
||||
}
|
||||
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
unsafe { GlobalSize(self.raw) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SmartGlobal {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
GlobalUnlock(self.raw).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ impl TextStyle {
|
||||
len,
|
||||
font: Font {
|
||||
family: self.font_family.clone(),
|
||||
features: Default::default(),
|
||||
features: self.font_features.clone(),
|
||||
fallbacks: self.font_fallbacks.clone(),
|
||||
weight: self.font_weight,
|
||||
style: self.font_style,
|
||||
|
||||
@@ -155,6 +155,7 @@ pub enum IconName {
|
||||
ListCollapse,
|
||||
ListTree,
|
||||
ListX,
|
||||
LoadCircle,
|
||||
LockOutlined,
|
||||
MagnifyingGlass,
|
||||
MailOpen,
|
||||
|
||||
@@ -24,13 +24,11 @@ indoc.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -14,7 +14,6 @@ use gpui::{
|
||||
pulsating_between,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use inline_completion::EditPredictionUsage;
|
||||
use language::{
|
||||
EditPredictionsMode, File, Language,
|
||||
language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
|
||||
@@ -30,7 +29,6 @@ use ui::{
|
||||
Clickable, ContextMenu, ContextMenuEntry, DocumentationSide, IconButton, IconButtonShape,
|
||||
Indicator, PopoverMenu, PopoverMenuHandle, ProgressBar, Tooltip, prelude::*,
|
||||
};
|
||||
use util::maybe;
|
||||
use workspace::{
|
||||
StatusItemView, Toast, Workspace, create_and_open_local_file, item::ItemHandle,
|
||||
notifications::NotificationId,
|
||||
@@ -237,11 +235,17 @@ impl Render for InlineCompletionButton {
|
||||
|
||||
let current_user_terms_accepted =
|
||||
self.user_store.read(cx).current_user_has_accepted_terms();
|
||||
let has_subscription = self.user_store.read(cx).current_plan().is_some()
|
||||
&& self.user_store.read(cx).subscription_period().is_some();
|
||||
|
||||
if !current_user_terms_accepted.unwrap_or(false) {
|
||||
if !has_subscription || !current_user_terms_accepted.unwrap_or(false) {
|
||||
let signed_in = current_user_terms_accepted.is_some();
|
||||
let tooltip_meta = if signed_in {
|
||||
"Read Terms of Service"
|
||||
if has_subscription {
|
||||
"Read Terms of Service"
|
||||
} else {
|
||||
"Choose a Plan"
|
||||
}
|
||||
} else {
|
||||
"Sign in to use"
|
||||
};
|
||||
@@ -399,64 +403,44 @@ impl InlineCompletionButton {
|
||||
let fs = self.fs.clone();
|
||||
let line_height = window.line_height();
|
||||
|
||||
if let Some(provider) = self.edit_prediction_provider.as_ref() {
|
||||
let usage = provider.usage(cx).or_else(|| {
|
||||
let user_store = self.user_store.read(cx);
|
||||
|
||||
maybe!({
|
||||
let amount = user_store.edit_predictions_usage_amount()?;
|
||||
let limit = user_store.edit_predictions_usage_limit()?.variant?;
|
||||
|
||||
Some(EditPredictionUsage {
|
||||
amount: amount as i32,
|
||||
limit: match limit {
|
||||
proto::usage_limit::Variant::Limited(limited) => {
|
||||
zed_llm_client::UsageLimit::Limited(limited.limit as i32)
|
||||
if let Some(usage) = self
|
||||
.edit_prediction_provider
|
||||
.as_ref()
|
||||
.and_then(|provider| provider.usage(cx))
|
||||
{
|
||||
menu = menu.header("Usage");
|
||||
menu = menu
|
||||
.custom_entry(
|
||||
move |_window, cx| {
|
||||
let used_percentage = match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
Some((usage.amount as f32 / limit as f32) * 100.)
|
||||
}
|
||||
proto::usage_limit::Variant::Unlimited(_) => {
|
||||
zed_llm_client::UsageLimit::Unlimited
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
if let Some(usage) = usage {
|
||||
menu = menu.header("Usage");
|
||||
menu = menu
|
||||
.custom_entry(
|
||||
move |_window, cx| {
|
||||
let used_percentage = match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
Some((usage.amount as f32 / limit as f32) * 100.)
|
||||
}
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.children(
|
||||
used_percentage.map(|percent| {
|
||||
ProgressBar::new("usage", percent, 100., cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new(match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", usage.amount)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", usage.amount),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.separator();
|
||||
}
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.children(
|
||||
used_percentage
|
||||
.map(|percent| ProgressBar::new("usage", percent, 100., cx)),
|
||||
)
|
||||
.child(
|
||||
Label::new(match usage.limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", usage.amount)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", usage.amount),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
},
|
||||
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
|
||||
)
|
||||
.separator();
|
||||
}
|
||||
|
||||
menu = menu.header("Show Edit Predictions For");
|
||||
@@ -851,7 +835,7 @@ async fn open_disabled_globs_setting_in_editor(
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
item.edit(edits, cx);
|
||||
}
|
||||
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
@@ -104,6 +104,10 @@ impl LanguageModelImage {
|
||||
// so this method is more of a rough guess.
|
||||
(width * height) / 750
|
||||
}
|
||||
|
||||
pub fn to_base64_url(&self) -> String {
|
||||
format!("data:image/png;base64,{}", self.source)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_as_base64(data: Arc<Image>, image: image::DynamicImage) -> Result<Vec<u8>> {
|
||||
|
||||
@@ -11,14 +11,26 @@ workspace = true
|
||||
[lib]
|
||||
path = "src/language_model_selector.rs"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
ordered-float.workspace = true
|
||||
picker.workspace = true
|
||||
proto.workspace = true
|
||||
ui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
use std::sync::Arc;
|
||||
use std::{cmp::Reverse, sync::Arc};
|
||||
|
||||
use collections::{HashSet, IndexMap};
|
||||
use feature_flags::ZedProFeatureFlag;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AnyElement, AnyView, App, Corner, DismissEvent, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, Task, WeakEntity, action_with_deprecated_aliases,
|
||||
Action, AnyElement, AnyView, App, BackgroundExecutor, Corner, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
action_with_deprecated_aliases,
|
||||
};
|
||||
use language_model::{
|
||||
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
|
||||
LanguageModelRegistry,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use proto::Plan;
|
||||
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*};
|
||||
@@ -322,6 +325,23 @@ struct GroupedModels {
|
||||
}
|
||||
|
||||
impl GroupedModels {
|
||||
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
|
||||
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
|
||||
for model in other {
|
||||
let provider = model.model.provider_id();
|
||||
if let Some(models) = other_by_provider.get_mut(&provider) {
|
||||
models.push(model);
|
||||
} else {
|
||||
other_by_provider.insert(provider, vec![model]);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
recommended,
|
||||
other: other_by_provider,
|
||||
}
|
||||
}
|
||||
|
||||
fn entries(&self) -> Vec<LanguageModelPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
@@ -349,6 +369,20 @@ impl GroupedModels {
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
fn model_infos(&self) -> Vec<ModelInfo> {
|
||||
let other = self
|
||||
.other
|
||||
.values()
|
||||
.flat_map(|model| model.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
self.recommended
|
||||
.iter()
|
||||
.chain(&other)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
enum LanguageModelPickerEntry {
|
||||
@@ -356,6 +390,78 @@ enum LanguageModelPickerEntry {
|
||||
Separator(SharedString),
|
||||
}
|
||||
|
||||
struct ModelMatcher {
|
||||
models: Vec<ModelInfo>,
|
||||
bg_executor: BackgroundExecutor,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
}
|
||||
|
||||
impl ModelMatcher {
|
||||
fn new(models: Vec<ModelInfo>, bg_executor: BackgroundExecutor) -> ModelMatcher {
|
||||
let candidates = Self::make_match_candidates(&models);
|
||||
Self {
|
||||
models,
|
||||
bg_executor,
|
||||
candidates,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fuzzy_search(&self, query: &str) -> Vec<ModelInfo> {
|
||||
let mut matches = self.bg_executor.block(match_strings(
|
||||
&self.candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
self.bg_executor.clone(),
|
||||
));
|
||||
|
||||
let sorting_key = |mat: &StringMatch| {
|
||||
let candidate = &self.candidates[mat.candidate_id];
|
||||
(Reverse(OrderedFloat(mat.score)), candidate.id)
|
||||
};
|
||||
matches.sort_unstable_by_key(sorting_key);
|
||||
|
||||
let matched_models: Vec<_> = matches
|
||||
.into_iter()
|
||||
.map(|mat| self.models[mat.candidate_id].clone())
|
||||
.collect();
|
||||
|
||||
matched_models
|
||||
}
|
||||
|
||||
pub fn exact_search(&self, query: &str) -> Vec<ModelInfo> {
|
||||
self.models
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
m.model
|
||||
.name()
|
||||
.0
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn make_match_candidates(model_infos: &Vec<ModelInfo>) -> Vec<StringMatchCandidate> {
|
||||
model_infos
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, model)| {
|
||||
StringMatchCandidate::new(
|
||||
index,
|
||||
&format!(
|
||||
"{}/{}",
|
||||
&model.model.provider_name().0,
|
||||
&model.model.name().0
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
type ListItem = AnyElement;
|
||||
|
||||
@@ -396,56 +502,45 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
) -> Task<()> {
|
||||
let all_models = self.all_models.clone();
|
||||
let current_index = self.selected_index;
|
||||
let bg_executor = cx.background_executor();
|
||||
|
||||
let language_model_registry = LanguageModelRegistry::global(cx);
|
||||
|
||||
let configured_providers = language_model_registry
|
||||
.read(cx)
|
||||
.providers()
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter(|provider| provider.is_authenticated(cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let configured_provider_ids = configured_providers
|
||||
.iter()
|
||||
.map(|provider| provider.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let recommended_models = all_models
|
||||
.recommended
|
||||
.iter()
|
||||
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let available_models = all_models
|
||||
.model_infos()
|
||||
.iter()
|
||||
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let matcher_rec = ModelMatcher::new(recommended_models, bg_executor.clone());
|
||||
let matcher_all = ModelMatcher::new(available_models, bg_executor.clone());
|
||||
|
||||
let recommended = matcher_rec.exact_search(&query);
|
||||
let all = matcher_all.fuzzy_search(&query);
|
||||
|
||||
let filtered_models = GroupedModels::new(all, recommended);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = cx
|
||||
.background_spawn(async move {
|
||||
let matches = |info: &ModelInfo| {
|
||||
info.model
|
||||
.name()
|
||||
.0
|
||||
.to_lowercase()
|
||||
.contains(&query.to_lowercase())
|
||||
};
|
||||
|
||||
let recommended_models = all_models
|
||||
.recommended
|
||||
.iter()
|
||||
.filter(|r| {
|
||||
configured_providers.contains(&r.model.provider_id()) && matches(r)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut other_models = IndexMap::default();
|
||||
for (provider_id, models) in &all_models.other {
|
||||
if configured_providers.contains(&provider_id) {
|
||||
other_models.insert(
|
||||
provider_id.clone(),
|
||||
models
|
||||
.iter()
|
||||
.filter(|m| matches(m))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
GroupedModels {
|
||||
recommended: recommended_models,
|
||||
other: other_models,
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries = filtered_models.entries();
|
||||
// Preserve selection focus
|
||||
@@ -607,3 +702,187 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AsyncApp, TestAppContext, http_client};
|
||||
use language_model::{
|
||||
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelRequest, LanguageModelToolChoice,
|
||||
};
|
||||
use ui::IconName;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestLanguageModel {
|
||||
name: LanguageModelName,
|
||||
id: LanguageModelId,
|
||||
provider_id: LanguageModelProviderId,
|
||||
provider_name: LanguageModelProviderName,
|
||||
}
|
||||
|
||||
impl TestLanguageModel {
|
||||
fn new(name: &str, provider: &str) -> Self {
|
||||
Self {
|
||||
name: LanguageModelName::from(name.to_string()),
|
||||
id: LanguageModelId::from(name.to_string()),
|
||||
provider_id: LanguageModelProviderId::from(provider.to_string()),
|
||||
provider_name: LanguageModelProviderName::from(provider.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageModel for TestLanguageModel {
|
||||
fn id(&self) -> LanguageModelId {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
fn name(&self) -> LanguageModelName {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn provider_id(&self) -> LanguageModelProviderId {
|
||||
self.provider_id.clone()
|
||||
}
|
||||
|
||||
fn provider_name(&self) -> LanguageModelProviderName {
|
||||
self.provider_name.clone()
|
||||
}
|
||||
|
||||
fn supports_tools(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_tool_choice(&self, _choice: LanguageModelToolChoice) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn telemetry_id(&self) -> String {
|
||||
format!("{}/{}", self.provider_id.0, self.name.0)
|
||||
}
|
||||
|
||||
fn max_token_count(&self) -> usize {
|
||||
1000
|
||||
}
|
||||
|
||||
fn count_tokens(
|
||||
&self,
|
||||
_: LanguageModelRequest,
|
||||
_: &App,
|
||||
) -> BoxFuture<'static, http_client::Result<usize>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn stream_completion(
|
||||
&self,
|
||||
_: LanguageModelRequest,
|
||||
_: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
http_client::Result<
|
||||
BoxStream<
|
||||
'static,
|
||||
http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
|
||||
>,
|
||||
>,
|
||||
> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
fn create_models(model_specs: Vec<(&str, &str)>) -> Vec<ModelInfo> {
|
||||
model_specs
|
||||
.into_iter()
|
||||
.map(|(provider, name)| ModelInfo {
|
||||
model: Arc::new(TestLanguageModel::new(name, provider)),
|
||||
icon: IconName::Ai,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn assert_models_eq(result: Vec<ModelInfo>, expected: Vec<&str>) {
|
||||
assert_eq!(
|
||||
result.len(),
|
||||
expected.len(),
|
||||
"Number of models doesn't match"
|
||||
);
|
||||
|
||||
for (i, expected_name) in expected.iter().enumerate() {
|
||||
assert_eq!(
|
||||
result[i].model.telemetry_id(),
|
||||
*expected_name,
|
||||
"Model at position {} doesn't match expected model",
|
||||
i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_exact_match(cx: &mut TestAppContext) {
|
||||
let models = create_models(vec![
|
||||
("zed", "Claude 3.7 Sonnet"),
|
||||
("zed", "Claude 3.7 Sonnet Thinking"),
|
||||
("zed", "gpt-4.1"),
|
||||
("zed", "gpt-4.1-nano"),
|
||||
("openai", "gpt-3.5-turbo"),
|
||||
("openai", "gpt-4.1"),
|
||||
("openai", "gpt-4.1-nano"),
|
||||
("ollama", "mistral"),
|
||||
("ollama", "deepseek"),
|
||||
]);
|
||||
let matcher = ModelMatcher::new(models, cx.background_executor.clone());
|
||||
|
||||
// The order of models should be maintained, case doesn't matter
|
||||
let results = matcher.exact_search("GPT-4.1");
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
"zed/gpt-4.1",
|
||||
"zed/gpt-4.1-nano",
|
||||
"openai/gpt-4.1",
|
||||
"openai/gpt-4.1-nano",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_models(vec![
|
||||
("zed", "Claude 3.7 Sonnet"),
|
||||
("zed", "Claude 3.7 Sonnet Thinking"),
|
||||
("zed", "gpt-4.1"),
|
||||
("zed", "gpt-4.1-nano"),
|
||||
("openai", "gpt-3.5-turbo"),
|
||||
("openai", "gpt-4.1"),
|
||||
("openai", "gpt-4.1-nano"),
|
||||
("ollama", "mistral"),
|
||||
("ollama", "deepseek"),
|
||||
]);
|
||||
let matcher = ModelMatcher::new(models, cx.background_executor.clone());
|
||||
|
||||
// Results should preserve models order whenever possible.
|
||||
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
|
||||
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
|
||||
// so it should appear first in the results.
|
||||
let results = matcher.fuzzy_search("41");
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
"zed/gpt-4.1",
|
||||
"openai/gpt-4.1",
|
||||
"zed/gpt-4.1-nano",
|
||||
"openai/gpt-4.1-nano",
|
||||
],
|
||||
);
|
||||
|
||||
// Model provider should be searchable as well
|
||||
let results = matcher.fuzzy_search("ol"); // meaning "ollama"
|
||||
assert_models_eq(results, vec!["ollama/mistral", "ollama/deepseek"]);
|
||||
|
||||
// Fuzzy search
|
||||
let results = matcher.fuzzy_search("z4n");
|
||||
assert_models_eq(results, vec!["zed/gpt-4.1-nano"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,17 +15,18 @@ path = "src/language_models.rs"
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
aws-config = { workspace = true, features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] }
|
||||
aws-credential-types = { workspace = true, features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws_http_client.workspace = true
|
||||
bedrock.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
copilot = { workspace = true, features = ["schemars"] }
|
||||
copilot.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
google_ai = { workspace = true, features = ["schemars"] }
|
||||
gpui.workspace = true
|
||||
@@ -39,7 +40,6 @@ mistral = { workspace = true, features = ["schemars"] }
|
||||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Context, Entity};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use provider::deepseek::DeepSeekLanguageModelProvider;
|
||||
@@ -21,8 +20,8 @@ use crate::provider::ollama::OllamaLanguageModelProvider;
|
||||
use crate::provider::open_ai::OpenAiLanguageModelProvider;
|
||||
pub use crate::settings::*;
|
||||
|
||||
pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
crate::settings::init(fs, cx);
|
||||
pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
||||
crate::settings::init(cx);
|
||||
let registry = LanguageModelRegistry::global(cx);
|
||||
registry.update(cx, |registry, cx| {
|
||||
register_language_model_providers(registry, user_store, client, cx);
|
||||
|
||||
@@ -38,7 +38,6 @@ pub struct AnthropicSettings {
|
||||
pub api_url: String,
|
||||
/// Extend Zed's list of Anthropic models.
|
||||
pub available_models: Vec<AvailableModel>,
|
||||
pub needs_setting_migration: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user