Compare commits
50 Commits
shell-quot
...
v0.183.6-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba9c033770 | ||
|
|
07ca5a6a33 | ||
|
|
0c13b42c3a | ||
|
|
bb1b132922 | ||
|
|
eefdcb36be | ||
|
|
2ac8a84c9f | ||
|
|
d35ffc7e10 | ||
|
|
093248ae05 | ||
|
|
21ff2bb39a | ||
|
|
843a621b1c | ||
|
|
bbe956f750 | ||
|
|
0179e4c511 | ||
|
|
df49cad705 | ||
|
|
13b3beb4d8 | ||
|
|
5f8efc9370 | ||
|
|
a1d643103a | ||
|
|
220d853dba | ||
|
|
911f329303 | ||
|
|
1bdcf318a6 | ||
|
|
d4f44c1137 | ||
|
|
e0dc131418 | ||
|
|
5054d0768d | ||
|
|
40add8682a | ||
|
|
d168fb5a16 | ||
|
|
6bfd2593c9 | ||
|
|
6db3b9c2e7 | ||
|
|
01daf6e7d4 | ||
|
|
e50872ca26 | ||
|
|
8b288aa98d | ||
|
|
aa1d400024 | ||
|
|
fd6e093827 | ||
|
|
91581d6d2b | ||
|
|
7102d40414 | ||
|
|
718e0a9851 | ||
|
|
2f4bd2a24b | ||
|
|
3aac735cb2 | ||
|
|
82fb597b95 | ||
|
|
07a0d91ea2 | ||
|
|
88ddd7be46 | ||
|
|
f701d69233 | ||
|
|
a8a99414d0 | ||
|
|
83ce1712dc | ||
|
|
9a54d111ef | ||
|
|
c2ff375787 | ||
|
|
1a81946137 | ||
|
|
36ca5ab7c2 | ||
|
|
ad3a319465 | ||
|
|
19b7c1ae89 | ||
|
|
9f8320f3a3 | ||
|
|
7c483b231d |
121
Cargo.lock
generated
121
Cargo.lock
generated
@@ -125,6 +125,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -324,7 +325,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -567,7 +568,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
@@ -704,6 +705,7 @@ dependencies = [
|
||||
"assistant_tool",
|
||||
"chrono",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"html_to_markdown",
|
||||
@@ -721,9 +723,11 @@ dependencies = [
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"web_search",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1881,7 +1885,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
@@ -3028,7 +3032,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry_events",
|
||||
@@ -3048,6 +3052,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3360,7 +3365,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"task",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -4477,7 +4482,7 @@ dependencies = [
|
||||
"optfield",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
@@ -5122,7 +5127,7 @@ dependencies = [
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
@@ -5973,7 +5978,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"time",
|
||||
@@ -6066,7 +6071,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -6172,7 +6177,7 @@ dependencies = [
|
||||
"slotmap",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"sum_tree",
|
||||
"taffy",
|
||||
"thiserror 2.0.12",
|
||||
@@ -6820,7 +6825,7 @@ name = "icons"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -7088,7 +7093,7 @@ dependencies = [
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -7674,11 +7679,12 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7734,7 +7740,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"thiserror 2.0.12",
|
||||
"tiktoken-rs",
|
||||
@@ -7742,6 +7748,7 @@ dependencies = [
|
||||
"ui",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8706,7 +8713,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -9553,7 +9560,7 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -12132,7 +12139,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"tracing",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -12660,7 +12667,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
@@ -13325,6 +13332,7 @@ dependencies = [
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"schemars",
|
||||
@@ -13705,7 +13713,7 @@ dependencies = [
|
||||
"settings",
|
||||
"simplelog",
|
||||
"story",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"title_bar",
|
||||
"ui",
|
||||
@@ -13787,7 +13795,16 @@ version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
"strum_macros 0.26.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13803,6 +13820,19 @@ dependencies = [
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.100",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -14418,7 +14448,7 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"serde_repr",
|
||||
"settings",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -14452,7 +14482,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"simplelog",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"vscode_theme",
|
||||
"workspace-hack",
|
||||
@@ -15453,7 +15483,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"story",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"ui_macros",
|
||||
"util",
|
||||
@@ -16586,6 +16616,36 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_search"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"gpui",
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web_search_providers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language_model",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"web_search",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-root-certs"
|
||||
version = "0.26.8"
|
||||
@@ -17624,7 +17684,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sqlez",
|
||||
"strum",
|
||||
"strum 0.27.1",
|
||||
"task",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
@@ -17769,7 +17829,7 @@ dependencies = [
|
||||
"sqlx-macros-core",
|
||||
"sqlx-postgres",
|
||||
"sqlx-sqlite",
|
||||
"strum",
|
||||
"strum 0.26.3",
|
||||
"subtle",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.100",
|
||||
@@ -18141,7 +18201,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.183.0"
|
||||
version = "0.183.6"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -18264,6 +18324,8 @@ dependencies = [
|
||||
"uuid",
|
||||
"vim",
|
||||
"vim_mode_setting",
|
||||
"web_search",
|
||||
"web_search_providers",
|
||||
"welcome",
|
||||
"windows 0.61.1",
|
||||
"winresource",
|
||||
@@ -18282,6 +18344,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -18328,12 +18391,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.4.1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bf21350eced858d129840589158a8f6895c4fa4327ae56dd8c7d6a98495bed4"
|
||||
checksum = "ad17428120f5ca776dc5195e2411a282f5150a26d5536671f8943c622c31274f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ members = [
|
||||
"crates/util_macros",
|
||||
"crates/vim",
|
||||
"crates/vim_mode_setting",
|
||||
"crates/web_search",
|
||||
"crates/web_search_providers",
|
||||
"crates/welcome",
|
||||
"crates/workspace",
|
||||
"crates/worktree",
|
||||
@@ -370,6 +372,8 @@ util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
web_search = { path = "crates/web_search" }
|
||||
web_search_providers = { path = "crates/web_search_providers" }
|
||||
welcome = { path = "crates/welcome" }
|
||||
workspace = { path = "crates/workspace" }
|
||||
worktree = { path = "crates/worktree" }
|
||||
@@ -536,7 +540,7 @@ smol = "2.0"
|
||||
sqlformat = "0.2"
|
||||
streaming-iterator = "0.1"
|
||||
strsim = "0.11"
|
||||
strum = { version = "0.26.0", features = ["derive"] }
|
||||
strum = { version = "0.27.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
sys-locale = "0.3.1"
|
||||
@@ -601,7 +605,7 @@ wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.4"
|
||||
zed_llm_client = "0.6.1"
|
||||
zstd = "0.11"
|
||||
metal = "0.29"
|
||||
|
||||
|
||||
@@ -630,6 +630,7 @@
|
||||
"ctrl-alt-n": "agent::NewTextThread",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-alt-p": "assistant::OpenPromptLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
|
||||
@@ -286,6 +286,7 @@
|
||||
"cmd-alt-n": "agent::NewTextThread",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-alt-p": "assistant::OpenPromptLibrary",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
|
||||
@@ -144,6 +144,19 @@ In Markdown, hash marks signify headings. For example:
|
||||
This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks.
|
||||
</style>
|
||||
|
||||
{{#if has_default_user_rules}}
|
||||
The user has specified the following rules that should be applied:
|
||||
{{#each default_user_rules}}
|
||||
|
||||
{{#if title}}
|
||||
Rules title: {{title}}
|
||||
{{/if}}
|
||||
``````
|
||||
{{contents}}
|
||||
``````
|
||||
{{/each}}
|
||||
|
||||
{{/if}}
|
||||
The user has opened a project that contains the following root directories/files. Whenever you specify a path in the project, it must be a relative path which begins with one of these root directories/files:
|
||||
|
||||
{{#each worktrees}}
|
||||
@@ -151,7 +164,7 @@ The user has opened a project that contains the following root directories/files
|
||||
{{/each}}
|
||||
{{#if has_rules}}
|
||||
|
||||
There are rules that apply to these root directories:
|
||||
There are project rules that apply to these root directories:
|
||||
{{#each worktrees}}
|
||||
{{#if rules_file}}
|
||||
|
||||
|
||||
@@ -652,7 +652,8 @@
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"thinking": true
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
@@ -678,7 +679,8 @@
|
||||
"regex_search": true,
|
||||
"rename": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true
|
||||
"thinking": true,
|
||||
"web_search": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,6 +90,7 @@ uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
use crate::context::{AssistantContext, ContextId};
|
||||
use crate::context::{AssistantContext, ContextId, format_context_as_string};
|
||||
use crate::context_picker::MentionLink;
|
||||
use crate::thread::{
|
||||
LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback,
|
||||
};
|
||||
use crate::thread_store::{RulesLoadingError, ThreadStore};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse};
|
||||
use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};
|
||||
use crate::{AssistantPanel, OpenActiveThreadAsMarkdown};
|
||||
use anyhow::Context as _;
|
||||
use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::{Editor, EditorElement, EditorStyle, MultiBuffer};
|
||||
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
|
||||
use gpui::{
|
||||
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,
|
||||
DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,
|
||||
MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment,
|
||||
ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription,
|
||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role, StopReason};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolUseId, RequestUsage, Role,
|
||||
StopReason,
|
||||
};
|
||||
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
|
||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
|
||||
use project::ProjectItem as _;
|
||||
@@ -38,6 +42,7 @@ use ui::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
use zed_actions::assistant::OpenPromptLibrary;
|
||||
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
@@ -60,6 +65,7 @@ pub struct ActiveThread {
|
||||
expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
|
||||
expanded_code_blocks: HashMap<(MessageId, usize), bool>,
|
||||
last_error: Option<ThreadError>,
|
||||
last_usage: Option<RequestUsage>,
|
||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||
copied_code_block_ids: HashSet<(MessageId, usize)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -498,11 +504,13 @@ fn render_markdown_code_block(
|
||||
|
||||
let codeblock_header = h_flex()
|
||||
.group("codeblock_header")
|
||||
.p_1()
|
||||
.py_1()
|
||||
.pl_1p5()
|
||||
.pr_1()
|
||||
.gap_1()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(codeblock_header_bg)
|
||||
.rounded_t_md()
|
||||
.children(label)
|
||||
@@ -596,7 +604,7 @@ fn render_markdown_code_block(
|
||||
.overflow_hidden()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(codeblock_header)
|
||||
.when(
|
||||
@@ -681,6 +689,9 @@ fn open_markdown_link(
|
||||
|
||||
struct EditMessageState {
|
||||
editor: Entity<Editor>,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
_subscription: Subscription,
|
||||
_update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
impl ActiveThread {
|
||||
@@ -726,6 +737,7 @@ impl ActiveThread {
|
||||
hide_scrollbar_task: None,
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
last_usage: None,
|
||||
copied_code_block_ids: HashSet::default(),
|
||||
notifications: Vec::new(),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -750,6 +762,10 @@ impl ActiveThread {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn context_store(&self) -> &Entity<ContextStore> {
|
||||
&self.context_store
|
||||
}
|
||||
|
||||
pub fn thread(&self) -> &Entity<Thread> {
|
||||
&self.thread
|
||||
}
|
||||
@@ -780,6 +796,17 @@ impl ActiveThread {
|
||||
self.last_error.take();
|
||||
}
|
||||
|
||||
pub fn last_usage(&self) -> Option<RequestUsage> {
|
||||
self.last_usage
|
||||
}
|
||||
|
||||
/// Returns the editing message id and the estimated token count in the content
|
||||
pub fn editing_message_id(&self) -> Option<(MessageId, usize)> {
|
||||
self.editing_message
|
||||
.as_ref()
|
||||
.map(|(id, state)| (*id, state.last_estimated_token_count.unwrap_or(0)))
|
||||
}
|
||||
|
||||
fn push_message(
|
||||
&mut self,
|
||||
id: &MessageId,
|
||||
@@ -857,6 +884,9 @@ impl ActiveThread {
|
||||
ThreadEvent::ShowError(error) => {
|
||||
self.last_error = Some(error.clone());
|
||||
}
|
||||
ThreadEvent::UsageUpdated(usage) => {
|
||||
self.last_usage = Some(*usage);
|
||||
}
|
||||
ThreadEvent::StreamedCompletion
|
||||
| ThreadEvent::SummaryGenerated
|
||||
| ThreadEvent::SummaryChanged => {
|
||||
@@ -943,8 +973,8 @@ impl ActiveThread {
|
||||
&tool_use.input,
|
||||
self.thread
|
||||
.read(cx)
|
||||
.tool_result(&tool_use.id)
|
||||
.map(|result| result.content.clone().into())
|
||||
.output_for_tool(&tool_use.id)
|
||||
.map(|output| output.clone().into())
|
||||
.unwrap_or("".into()),
|
||||
cx,
|
||||
);
|
||||
@@ -1125,15 +1155,93 @@ impl ActiveThread {
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor
|
||||
});
|
||||
let subscription = cx.subscribe(&editor, |this, _, event, cx| match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
this.update_editing_message_token_count(true, cx);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
self.editing_message = Some((
|
||||
message_id,
|
||||
EditMessageState {
|
||||
editor: editor.clone(),
|
||||
last_estimated_token_count: None,
|
||||
_subscription: subscription,
|
||||
_update_token_count_task: None,
|
||||
},
|
||||
));
|
||||
self.update_editing_message_token_count(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_editing_message_token_count(&mut self, debounce: bool, cx: &mut Context<Self>) {
|
||||
let Some((message_id, state)) = self.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
state._update_token_count_task.take();
|
||||
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
state.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
|
||||
let editor = state.editor.clone();
|
||||
let thread = self.thread.clone();
|
||||
let message_id = *message_id;
|
||||
|
||||
state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let context = thread.read(cx).context_for_message(message_id);
|
||||
let new_context = thread.read(cx).filter_new_context(context);
|
||||
let context_text =
|
||||
format_context_as_string(new_context, cx).unwrap_or(String::new());
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
let content = context_text + &message_text;
|
||||
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
cache: false,
|
||||
}],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let Some((_message_id, state)) = this.editing_message.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editing_message.take();
|
||||
cx.notify();
|
||||
@@ -1171,6 +1279,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model.model, RequestKind::Chat, cx)
|
||||
});
|
||||
cx.notify();
|
||||
@@ -1402,16 +1511,23 @@ impl ActiveThread {
|
||||
let editor_bg_color = colors.editor_background;
|
||||
let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
|
||||
|
||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileCode)
|
||||
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
.on_click(|_event, window, cx| {
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(OpenActiveThreadAsMarkdown), cx)
|
||||
});
|
||||
|
||||
let feedback_container = h_flex().py_2().px_4().gap_1().justify_between();
|
||||
// For all items that should be aligned with the Assistant's response.
|
||||
const RESPONSE_PADDING_X: Pixels = px(18.);
|
||||
|
||||
let feedback_container = h_flex()
|
||||
.py_2()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_1()
|
||||
.justify_between();
|
||||
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
|
||||
Some(feedback) => feedback_container
|
||||
.child(
|
||||
@@ -1612,9 +1728,9 @@ impl ActiveThread {
|
||||
this.pt_4()
|
||||
}
|
||||
})
|
||||
.pb_4()
|
||||
.pl_2()
|
||||
.pr_2p5()
|
||||
.pb_4()
|
||||
.child(
|
||||
v_flex()
|
||||
.bg(colors.editor_background)
|
||||
@@ -1675,6 +1791,9 @@ impl ActiveThread {
|
||||
"confirm-edit-message",
|
||||
"Regenerate",
|
||||
)
|
||||
.disabled(
|
||||
edit_message_editor.read(cx).is_empty(cx),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
@@ -1718,9 +1837,8 @@ impl ActiveThread {
|
||||
),
|
||||
Role::Assistant => v_flex()
|
||||
.id(("message-container", ix))
|
||||
.ml_2p5()
|
||||
.pl_2()
|
||||
.pr_4()
|
||||
.px(RESPONSE_PADDING_X)
|
||||
.gap_2()
|
||||
.children(message_content)
|
||||
.when(has_tool_uses, |parent| {
|
||||
parent.children(
|
||||
@@ -1737,6 +1855,15 @@ impl ActiveThread {
|
||||
),
|
||||
};
|
||||
|
||||
let after_editing_message = self
|
||||
.editing_message
|
||||
.as_ref()
|
||||
.map_or(false, |(editing_message_id, _)| {
|
||||
message_id > *editing_message_id
|
||||
});
|
||||
|
||||
let panel_background = cx.theme().colors().panel_background;
|
||||
|
||||
v_flex()
|
||||
.w_full()
|
||||
.when_some(checkpoint, |parent, checkpoint| {
|
||||
@@ -1900,6 +2027,18 @@ impl ActiveThread {
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(after_editing_message, |parent| {
|
||||
// Backdrop to dim out the whole thread below the editing user message
|
||||
parent.relative().child(
|
||||
div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(panel_background)
|
||||
.opacity(0.8),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -1926,6 +2065,15 @@ impl ActiveThread {
|
||||
None
|
||||
};
|
||||
|
||||
let message_role = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(message_id)
|
||||
.map(|m| m.role)
|
||||
.unwrap_or(Role::User);
|
||||
|
||||
let is_assistant = message_role == Role::Assistant;
|
||||
|
||||
v_flex()
|
||||
.text_ui(cx)
|
||||
.gap_2()
|
||||
@@ -1946,80 +2094,100 @@ impl ActiveThread {
|
||||
cx,
|
||||
)
|
||||
.into_any_element(),
|
||||
RenderedMessageSegment::Text(markdown) => div()
|
||||
.child(
|
||||
MarkdownElement::new(
|
||||
markdown.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Custom {
|
||||
render: Arc::new({
|
||||
let workspace = workspace.clone();
|
||||
let active_thread = cx.entity();
|
||||
move |kind, parsed_markdown, range, metadata, window, cx| {
|
||||
render_markdown_code_block(
|
||||
message_id,
|
||||
range.start,
|
||||
kind,
|
||||
parsed_markdown,
|
||||
metadata,
|
||||
active_thread.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
transform: Some(Arc::new({
|
||||
let active_thread = cx.entity();
|
||||
move |el, range, metadata, _, cx| {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, range.start))
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
RenderedMessageSegment::Text(markdown) => {
|
||||
let markdown_element = MarkdownElement::new(
|
||||
markdown.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
);
|
||||
|
||||
if is_expanded
|
||||
|| metadata.line_count
|
||||
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
|
||||
{
|
||||
return el;
|
||||
let markdown_element = if is_assistant {
|
||||
markdown_element.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Custom {
|
||||
render: Arc::new({
|
||||
let workspace = workspace.clone();
|
||||
let active_thread = cx.entity();
|
||||
move |kind,
|
||||
parsed_markdown,
|
||||
range,
|
||||
metadata,
|
||||
window,
|
||||
cx| {
|
||||
render_markdown_code_block(
|
||||
message_id,
|
||||
range.start,
|
||||
kind,
|
||||
parsed_markdown,
|
||||
metadata,
|
||||
active_thread.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
el.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_1_4()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().editor_background,
|
||||
}),
|
||||
transform: Some(Arc::new({
|
||||
let active_thread = cx.entity();
|
||||
move |el, range, metadata, _, cx| {
|
||||
let is_expanded = active_thread
|
||||
.read(cx)
|
||||
.expanded_code_blocks
|
||||
.get(&(message_id, range.start))
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_expanded
|
||||
|| metadata.line_count
|
||||
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
|
||||
{
|
||||
return el;
|
||||
}
|
||||
el.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_1_4()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.opacity(0.),
|
||||
1.,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
})),
|
||||
})
|
||||
.on_url_click({
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background,
|
||||
0.,
|
||||
),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.opacity(0.),
|
||||
1.,
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
})),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
markdown_element.code_block_renderer(
|
||||
markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: true,
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
div()
|
||||
.child(markdown_element.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any_element(),
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -2279,6 +2447,10 @@ impl ActiveThread {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
return card.render(&tool_use.status, window, cx);
|
||||
}
|
||||
|
||||
let is_open = self
|
||||
.expanded_tool_uses
|
||||
.get(&tool_use.id)
|
||||
@@ -2343,6 +2515,10 @@ impl ActiveThread {
|
||||
rendered.input.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
@@ -2369,12 +2545,17 @@ impl ActiveThread {
|
||||
rendered.output.clone(),
|
||||
tool_use_markdown_style(window, cx),
|
||||
)
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click({
|
||||
let workspace = self.workspace.clone();
|
||||
move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
)),
|
||||
),
|
||||
@@ -2431,6 +2612,7 @@ impl ActiveThread {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
})),
|
||||
),
|
||||
),
|
||||
@@ -2468,11 +2650,10 @@ impl ActiveThread {
|
||||
))
|
||||
};
|
||||
|
||||
div().map(|element| {
|
||||
v_flex().gap_1().mb_3().map(|element| {
|
||||
if !edit_tools {
|
||||
element.child(
|
||||
v_flex()
|
||||
.my_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
@@ -2494,7 +2675,7 @@ impl ActiveThread {
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex().pr_8().text_ui_sm(cx).children(
|
||||
h_flex().pr_8().text_size(rems(0.8125)).children(
|
||||
rendered_tool_use.map(|rendered| MarkdownElement::new(rendered.label, tool_use_markdown_style(window, cx)).on_url_click({let workspace = self.workspace.clone(); move |text, window, cx| {
|
||||
open_markdown_link(text, workspace.clone(), window, cx);
|
||||
}}))
|
||||
@@ -2544,7 +2725,7 @@ impl ActiveThread {
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.my_3()
|
||||
.mb_2()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
@@ -2761,7 +2942,7 @@ impl ActiveThread {
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
}).into_any_element()
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
@@ -2771,53 +2952,116 @@ impl ActiveThread {
|
||||
return div().into_any();
|
||||
};
|
||||
|
||||
let default_user_rules_text = if project_context.default_user_rules.is_empty() {
|
||||
None
|
||||
} else if project_context.default_user_rules.len() == 1 {
|
||||
let user_rules = &project_context.default_user_rules[0];
|
||||
|
||||
match user_rules.title.as_ref() {
|
||||
Some(title) => Some(format!("Using \"{title}\" user rule")),
|
||||
None => Some("Using user rule".into()),
|
||||
}
|
||||
} else {
|
||||
Some(format!(
|
||||
"Using {} user rules",
|
||||
project_context.default_user_rules.len()
|
||||
))
|
||||
};
|
||||
|
||||
let first_default_user_rules_id = project_context
|
||||
.default_user_rules
|
||||
.first()
|
||||
.map(|user_rules| user_rules.uuid);
|
||||
|
||||
let rules_files = project_context
|
||||
.worktrees
|
||||
.iter()
|
||||
.filter_map(|worktree| worktree.rules_file.as_ref())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let label_text = match rules_files.as_slice() {
|
||||
&[] => return div().into_any(),
|
||||
&[rules_file] => {
|
||||
format!("Using {:?} file", rules_file.path_in_worktree)
|
||||
}
|
||||
rules_files => {
|
||||
format!("Using {} rules files", rules_files.len())
|
||||
}
|
||||
let rules_file_text = match rules_files.as_slice() {
|
||||
&[] => None,
|
||||
&[rules_file] => Some(format!(
|
||||
"Using project {:?} file",
|
||||
rules_file.path_in_worktree
|
||||
)),
|
||||
rules_files => Some(format!("Using {} project rules files", rules_files.len())),
|
||||
};
|
||||
|
||||
div()
|
||||
if default_user_rules_text.is_none() && rules_file_text.is_none() {
|
||||
return div().into_any();
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.pt_2()
|
||||
.px_2p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
.gap_1()
|
||||
.when_some(
|
||||
default_user_rules_text,
|
||||
|parent, default_user_rules_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(label_text)
|
||||
Label::new(default_user_rules_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx),
|
||||
.truncate()
|
||||
.buffer_font(cx)
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-prompt-library", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
// TODO: Figure out a way to pass focus handle here so we can display the `OpenPromptLibrary` keybinding
|
||||
.tooltip(Tooltip::text("View User Rules"))
|
||||
.on_click(move |_event, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(OpenPromptLibrary {
|
||||
prompt_to_focus: first_default_user_rules_id,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.on_click(cx.listener(Self::handle_open_rules))
|
||||
.tooltip(Tooltip::text("View Rules")),
|
||||
),
|
||||
},
|
||||
)
|
||||
.when_some(rules_file_text, |parent, rules_file_text| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(IconName::File)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(rules_file_text)
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.buffer_font(cx)
|
||||
.ml_1p5()
|
||||
.mr_0p5(),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("open-rule", IconName::ArrowUpRightAlt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Ignored)
|
||||
.on_click(cx.listener(Self::handle_open_rules))
|
||||
.tooltip(Tooltip::text("View Rules")),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -2953,6 +3197,12 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActiveThreadEvent {
|
||||
EditingMessageTokenCountChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<ActiveThreadEvent> for ActiveThread {}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
@@ -3028,28 +3278,21 @@ pub(crate) fn open_context(
|
||||
.start
|
||||
.to_point(&snapshot);
|
||||
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(
|
||||
target_position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if let Some(project_path) = excerpt_context
|
||||
.context_buffer
|
||||
.buffer
|
||||
.read(cx)
|
||||
.project_path(cx)
|
||||
{
|
||||
let snapshot = excerpt_context.context_buffer.buffer.read(cx).snapshot();
|
||||
let target_position = excerpt_context.range.start.to_point(&snapshot);
|
||||
|
||||
open_editor_at_position(project_path, target_position, &workspace, window, cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
@@ -3070,3 +3313,29 @@ pub(crate) fn open_context(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_editor_at_position(
|
||||
project_path: project::ProjectPath,
|
||||
target_position: Point,
|
||||
workspace: &Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<()> {
|
||||
let open_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(project_path, None, true, window, cx)
|
||||
});
|
||||
window.spawn(cx, async move |cx| {
|
||||
if let Some(active_editor) = open_task
|
||||
.await
|
||||
.log_err()
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
{
|
||||
active_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.go_to_singleton_buffer_point(target_position, window, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -922,6 +922,7 @@ mod tests {
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -951,7 +952,8 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
|
||||
@@ -132,7 +132,11 @@ impl AssistantConfiguration {
|
||||
.cloned();
|
||||
|
||||
v_flex()
|
||||
.pt_3()
|
||||
.pb_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -144,7 +148,7 @@ impl AssistantConfiguration {
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new(provider_name.clone())),
|
||||
.child(Label::new(provider_name.clone()).size(LabelSize::Large)),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
@@ -169,20 +173,12 @@ impl AssistantConfiguration {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.p(DynamicSpacing::Base08.rems(cx))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.map(|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}",
|
||||
)))),
|
||||
}),
|
||||
)
|
||||
.map(|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}",
|
||||
)))),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
@@ -199,7 +195,7 @@ impl AssistantConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Headline::new("LLM Providers").size(HeadlineSize::Small))
|
||||
.child(Headline::new("LLM Providers"))
|
||||
.child(
|
||||
Label::new("Add at least one provider to use AI-powered features.")
|
||||
.color(Color::Muted),
|
||||
@@ -215,21 +211,16 @@ impl AssistantConfiguration {
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AssistantSettings::get_global(cx).always_allow_tool_actions;
|
||||
|
||||
const HEADING: &str = "Allow running tools without asking for confirmation";
|
||||
const HEADING: &str = "Allow running editing tools without asking for confirmation";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Headline::new("General Settings").size(HeadlineSize::Small))
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_2p5()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
@@ -277,10 +268,7 @@ impl AssistantConfiguration {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Headline::new("Model Context Protocol (MCP) Servers")
|
||||
.size(HeadlineSize::Small),
|
||||
)
|
||||
.child(Headline::new("Model Context Protocol (MCP) Servers"))
|
||||
.child(Label::new(SUBHEADING).color(Color::Muted)),
|
||||
)
|
||||
.children(context_servers.into_iter().map(|context_server| {
|
||||
@@ -301,9 +289,9 @@ impl AssistantConfiguration {
|
||||
v_flex()
|
||||
.id(SharedString::from(context_server.id()))
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.rounded_md()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(cx.theme().colors().background.opacity(0.25))
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
@@ -386,34 +374,28 @@ impl AssistantConfiguration {
|
||||
return parent;
|
||||
}
|
||||
|
||||
parent.child(v_flex().children(tools.into_iter().enumerate().map(
|
||||
|(ix, tool)| {
|
||||
parent.child(v_flex().py_1p5().px_1().gap_1().children(
|
||||
tools.into_iter().enumerate().map(|(ix, tool)| {
|
||||
h_flex()
|
||||
.id("tool-item")
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.id(("tool-item", ix))
|
||||
.px_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.when(ix < tool_count - 1, |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new(tool.name())
|
||||
.buffer_font(cx)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(("tool-description", ix), IconName::Info)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text(tool.description())),
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Ignored),
|
||||
)
|
||||
},
|
||||
)))
|
||||
.tooltip(Tooltip::text(tool.description()))
|
||||
}),
|
||||
))
|
||||
})
|
||||
}))
|
||||
.child(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -5,14 +6,14 @@ use std::time::Duration;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
|
||||
make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
humanize_token_count, make_lsp_adapter_delegate, render_remaining_tokens,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
|
||||
use client::zed_urls;
|
||||
use editor::{Editor, EditorEvent, MultiBuffer};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
|
||||
@@ -24,7 +25,8 @@ use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{PromptBuilder, PromptId};
|
||||
use proto::Plan;
|
||||
use settings::{Settings, update_settings_file};
|
||||
use time::UtcOffset;
|
||||
use ui::{
|
||||
@@ -36,13 +38,14 @@ use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
use zed_actions::assistant::{OpenPromptLibrary, ToggleFocus};
|
||||
|
||||
use crate::active_thread::ActiveThread;
|
||||
use crate::active_thread::{ActiveThread, ActiveThreadEvent};
|
||||
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::message_editor::MessageEditor;
|
||||
use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::UsageBanner;
|
||||
use crate::{
|
||||
AgentDiff, ExpandMessageEditor, InlineAssistant, NewTextThread, NewThread,
|
||||
OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
|
||||
@@ -80,7 +83,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -111,7 +114,9 @@ enum ActiveView {
|
||||
change_title_editor: Entity<Editor>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
PromptEditor,
|
||||
PromptEditor {
|
||||
context_editor: Entity<ContextEditor>,
|
||||
},
|
||||
History,
|
||||
Configuration,
|
||||
}
|
||||
@@ -180,10 +185,9 @@ pub struct AssistantPanel {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
thread: Entity<ActiveThread>,
|
||||
_thread_subscription: Subscription,
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_active_thread_subscriptions: Vec<Subscription>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
context_editor: Option<Entity<ContextEditor>>,
|
||||
configuration: Option<Entity<AssistantConfiguration>>,
|
||||
configuration_subscription: Option<Subscription>,
|
||||
local_timezone: UtcOffset,
|
||||
@@ -209,7 +213,7 @@ impl AssistantPanel {
|
||||
let project = workspace.project().clone();
|
||||
ThreadStore::load(project, tools.clone(), prompt_builder.clone(), cx)
|
||||
})?
|
||||
.await;
|
||||
.await?;
|
||||
|
||||
let slash_commands = Arc::new(SlashCommandWorkingSet::default());
|
||||
let context_store = workspace
|
||||
@@ -263,6 +267,13 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
let history_store =
|
||||
cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
|
||||
|
||||
@@ -287,6 +298,12 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription = cx.subscribe(&thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
active_view,
|
||||
workspace,
|
||||
@@ -295,10 +312,13 @@ impl AssistantPanel {
|
||||
language_registry,
|
||||
thread_store: thread_store.clone(),
|
||||
thread,
|
||||
_thread_subscription: thread_subscription,
|
||||
message_editor,
|
||||
_active_thread_subscriptions: vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
],
|
||||
context_store,
|
||||
context_editor: None,
|
||||
configuration: None,
|
||||
configuration_subscription: None,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
@@ -381,6 +401,13 @@ impl AssistantPanel {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
@@ -393,12 +420,12 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
|
||||
self._thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&self.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
@@ -412,11 +439,22 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
self.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
self._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_active_view(ActiveView::PromptEditor, window, cx);
|
||||
|
||||
let context = self
|
||||
.context_store
|
||||
.update(cx, |context_store, cx| context_store.create(cx));
|
||||
@@ -424,7 +462,7 @@ impl AssistantPanel {
|
||||
.log_err()
|
||||
.flatten();
|
||||
|
||||
self.context_editor = Some(cx.new(|cx| {
|
||||
let context_editor = cx.new(|cx| {
|
||||
let mut editor = ContextEditor::for_context(
|
||||
context,
|
||||
self.fs.clone(),
|
||||
@@ -436,16 +474,21 @@ impl AssistantPanel {
|
||||
);
|
||||
editor.insert_default_prompt(window, cx);
|
||||
editor
|
||||
}));
|
||||
});
|
||||
|
||||
if let Some(context_editor) = self.context_editor.as_ref() {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
self.set_active_view(
|
||||
ActiveView::PromptEditor {
|
||||
context_editor: context_editor.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
_: &OpenPromptLibrary,
|
||||
action: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -459,6 +502,7 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -507,8 +551,13 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
this.set_active_view(ActiveView::PromptEditor, window, cx);
|
||||
this.context_editor = Some(editor);
|
||||
this.set_active_view(
|
||||
ActiveView::PromptEditor {
|
||||
context_editor: editor,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
anyhow::Ok(())
|
||||
})??;
|
||||
@@ -537,6 +586,13 @@ impl AssistantPanel {
|
||||
Some(this.thread_store.downgrade()),
|
||||
)
|
||||
});
|
||||
let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
|
||||
if let ThreadEvent::MessageAdded(_) = &event {
|
||||
// needed to leave empty state
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
thread.clone(),
|
||||
@@ -548,6 +604,14 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let active_thread_subscription =
|
||||
cx.subscribe(&this.thread, |_, _, event, cx| match &event {
|
||||
ActiveThreadEvent::EditingMessageTokenCountChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this.message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
this.fs.clone(),
|
||||
@@ -560,6 +624,19 @@ impl AssistantPanel {
|
||||
)
|
||||
});
|
||||
this.message_editor.focus_handle(cx).focus(window);
|
||||
|
||||
let message_editor_subscription =
|
||||
cx.subscribe(&this.message_editor, |_, _, event, cx| match event {
|
||||
MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
this._active_thread_subscriptions = vec![
|
||||
thread_subscription,
|
||||
active_thread_subscription,
|
||||
message_editor_subscription,
|
||||
];
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -711,8 +788,15 @@ impl AssistantPanel {
|
||||
.update(cx, |this, cx| this.delete_thread(thread_id, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn has_active_thread(&self) -> bool {
|
||||
matches!(self.active_view, ActiveView::Thread { .. })
|
||||
}
|
||||
|
||||
pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
|
||||
self.context_editor.clone()
|
||||
match &self.active_view {
|
||||
ActiveView::PromptEditor { context_editor } => Some(context_editor.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn delete_context(
|
||||
@@ -750,16 +834,10 @@ impl AssistantPanel {
|
||||
|
||||
impl Focusable for AssistantPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match self.active_view {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
ActiveView::PromptEditor => {
|
||||
if let Some(context_editor) = self.context_editor.as_ref() {
|
||||
context_editor.focus_handle(cx)
|
||||
} else {
|
||||
cx.focus_handle()
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor { context_editor } => context_editor.focus_handle(cx),
|
||||
ActiveView::Configuration => {
|
||||
if let Some(configuration) = self.configuration.as_ref() {
|
||||
configuration.focus_handle(cx)
|
||||
@@ -852,7 +930,7 @@ impl Panel for AssistantPanel {
|
||||
}
|
||||
|
||||
impl AssistantPanel {
|
||||
fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
|
||||
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
|
||||
|
||||
let content = match &self.active_view {
|
||||
@@ -883,15 +961,8 @@ impl AssistantPanel {
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
let title = self
|
||||
.context_editor
|
||||
.as_ref()
|
||||
.map(|context_editor| {
|
||||
SharedString::from(context_editor.read(cx).title(cx).to_string())
|
||||
})
|
||||
.unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
|
||||
|
||||
ActiveView::PromptEditor { context_editor } => {
|
||||
let title = SharedString::from(context_editor.read(cx).title(cx).to_string());
|
||||
Label::new(title).ml_2().truncate().into_any_element()
|
||||
}
|
||||
ActiveView::History => Label::new("History").truncate().into_any_element(),
|
||||
@@ -912,21 +983,18 @@ impl AssistantPanel {
|
||||
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_thread = self.thread.read(cx);
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let token_usage = thread.total_token_usage(cx);
|
||||
let thread_id = thread.id().clone();
|
||||
|
||||
let is_generating = thread.is_generating();
|
||||
let is_empty = active_thread.is_empty();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let is_history = matches!(self.active_view, ActiveView::History);
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
ActiveView::Thread { .. } => !is_empty,
|
||||
ActiveView::PromptEditor => self.context_editor.is_some(),
|
||||
ActiveView::PromptEditor { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let go_back_button = match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => Some(
|
||||
div().pl_1().child(
|
||||
@@ -973,69 +1041,9 @@ impl AssistantPanel {
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap_2()
|
||||
.when(show_token_count, |parent| match self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
if token_usage.total == 0 {
|
||||
return parent;
|
||||
}
|
||||
|
||||
let token_color = match token_usage.ratio {
|
||||
TokenUsageRatio::Normal => Color::Muted,
|
||||
TokenUsageRatio::Warning => Color::Warning,
|
||||
TokenUsageRatio::Exceeded => Color::Error,
|
||||
};
|
||||
|
||||
parent.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(assistant_context_editor::humanize_token_count(
|
||||
token_usage.total,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_color)
|
||||
.map(|label| {
|
||||
if is_generating {
|
||||
label
|
||||
.with_animation(
|
||||
"used-tokens-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(
|
||||
0.6, 1.,
|
||||
)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
label.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Label::new("/").size(LabelSize::Small).color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(assistant_context_editor::humanize_token_count(
|
||||
token_usage.max,
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
}
|
||||
ActiveView::PromptEditor => {
|
||||
let Some(editor) = self.context_editor.as_ref() else {
|
||||
return parent;
|
||||
};
|
||||
let Some(element) = render_remaining_tokens(editor, cx) else {
|
||||
return parent;
|
||||
};
|
||||
parent.child(element)
|
||||
}
|
||||
_ => parent,
|
||||
})
|
||||
.when(show_token_count, |parent|
|
||||
parent.children(self.render_token_count(&thread, cx))
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
@@ -1112,16 +1120,16 @@ impl AssistantPanel {
|
||||
"New Text Thread",
|
||||
NewTextThread.boxed_clone(),
|
||||
)
|
||||
.action("Settings", OpenConfiguration.boxed_clone())
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary::default()))
|
||||
.action("Settings", Box::new(OpenConfiguration))
|
||||
.separator()
|
||||
.action(
|
||||
"Install MCPs",
|
||||
zed_actions::Extensions {
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
}
|
||||
.boxed_clone(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
))
|
||||
@@ -1131,6 +1139,110 @@ impl AssistantPanel {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
|
||||
let is_generating = thread.is_generating();
|
||||
let message_editor = self.message_editor.read(cx);
|
||||
|
||||
let conversation_token_usage = thread.total_token_usage(cx);
|
||||
let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
|
||||
self.thread.read(cx).editing_message_id()
|
||||
{
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id, cx)
|
||||
.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
} else {
|
||||
let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
|
||||
let combined = conversation_token_usage.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
};
|
||||
|
||||
let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { .. } => {
|
||||
if total_token_usage.total == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let token_color = match total_token_usage.ratio() {
|
||||
TokenUsageRatio::Normal if is_estimating => Color::Default,
|
||||
TokenUsageRatio::Normal => Color::Muted,
|
||||
TokenUsageRatio::Warning => Color::Warning,
|
||||
TokenUsageRatio::Exceeded => Color::Error,
|
||||
};
|
||||
|
||||
let token_count = h_flex()
|
||||
.id("token-count")
|
||||
.flex_shrink_0()
|
||||
.gap_0p5()
|
||||
.when(!is_generating && is_estimating, |parent| {
|
||||
parent
|
||||
.child(
|
||||
h_flex()
|
||||
.mr_1()
|
||||
.size_2p5()
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text.opacity(0.1))
|
||||
.child(
|
||||
div().size_1().rounded_full().bg(cx.theme().colors().text),
|
||||
),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Estimated New Token Count",
|
||||
None,
|
||||
format!(
|
||||
"Current Conversation Tokens: {}",
|
||||
humanize_token_count(conversation_token_usage.total)
|
||||
),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
Label::new(humanize_token_count(total_token_usage.total))
|
||||
.size(LabelSize::Small)
|
||||
.color(token_color)
|
||||
.map(|label| {
|
||||
if is_generating || is_waiting_to_update_token_count {
|
||||
label
|
||||
.with_animation(
|
||||
"used-tokens-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
label.into_any_element()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(humanize_token_count(total_token_usage.max))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
Some(token_count)
|
||||
}
|
||||
ActiveView::PromptEditor { context_editor } => {
|
||||
let element = render_remaining_tokens(context_editor, cx)?;
|
||||
|
||||
Some(element.into_any_element())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_active_thread_or_empty_state(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
@@ -1431,6 +1543,12 @@ impl AssistantPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let usage = self.thread.read(cx).last_usage()?;
|
||||
|
||||
Some(UsageBanner::new(zed_llm_client::Plan::ZedProTrial, usage.amount).into_any_element())
|
||||
}
|
||||
|
||||
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let last_error = self.thread.read(cx).last_error()?;
|
||||
|
||||
@@ -1449,6 +1567,9 @@ impl AssistantPanel {
|
||||
ThreadError::MaxMonthlySpendReached => {
|
||||
self.render_max_monthly_spend_reached_error(cx)
|
||||
}
|
||||
ThreadError::ModelRequestLimitReached { plan } => {
|
||||
self.render_model_request_limit_reached_error(plan, cx)
|
||||
}
|
||||
ThreadError::Message { header, message } => {
|
||||
self.render_error_message(header, message, cx)
|
||||
}
|
||||
@@ -1551,6 +1672,71 @@ impl AssistantPanel {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
&self,
|
||||
plan: Plan,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let error_message = match plan {
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
Plan::ZedProTrial => {
|
||||
"Model request limit reached. Upgrade to Zed Pro for more requests."
|
||||
}
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
};
|
||||
let call_to_action = match plan {
|
||||
Plan::ZedPro => "Upgrade to usage-based billing",
|
||||
Plan::ZedProTrial => "Upgrade to Zed Pro",
|
||||
Plan::Free => "Upgrade to Zed Pro",
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.child(
|
||||
Button::new("subscribe", call_to_action).on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
|this, _, _, cx| {
|
||||
this.thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
},
|
||||
))),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
&self,
|
||||
header: SharedString,
|
||||
@@ -1593,7 +1779,7 @@ impl AssistantPanel {
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
if matches!(self.active_view, ActiveView::PromptEditor) {
|
||||
if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
|
||||
key_context.add("prompt_editor");
|
||||
}
|
||||
key_context
|
||||
@@ -1621,13 +1807,14 @@ impl Render for AssistantPanel {
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.on_action(cx.listener(Self::go_back))
|
||||
.child(self.render_toolbar(window, cx))
|
||||
.map(|parent| match self.active_view {
|
||||
.map(|parent| match &self.active_view {
|
||||
ActiveView::Thread { .. } => parent
|
||||
.child(self.render_active_thread_or_empty_state(window, cx))
|
||||
.children(self.render_usage_banner(cx))
|
||||
.child(h_flex().child(self.message_editor.clone()))
|
||||
.children(self.render_last_error(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
|
||||
ActiveView::PromptEditor { context_editor } => parent.child(context_editor.clone()),
|
||||
ActiveView::Configuration => parent.children(self.configuration.clone()),
|
||||
})
|
||||
}
|
||||
@@ -1692,7 +1879,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Entity<ContextEditor>> {
|
||||
let panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
panel.update(cx, |panel, _cx| panel.context_editor.clone())
|
||||
panel.read(cx).active_context_editor()
|
||||
}
|
||||
|
||||
fn open_saved_context(
|
||||
@@ -1723,10 +1910,59 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
|
||||
fn quote_selection(
|
||||
&self,
|
||||
_workspace: &mut Workspace,
|
||||
_creases: Vec<(String, String)>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Workspace>,
|
||||
workspace: &mut Workspace,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !panel.focus_handle(cx).contains_focused(window, cx) {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
cx.defer_in(window, move |panel, window, cx| {
|
||||
if panel.has_active_thread() {
|
||||
panel.thread.update(cx, |thread, cx| {
|
||||
thread.context_store().update(cx, |store, cx| {
|
||||
let buffer = buffer.read(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.flat_map(|range| {
|
||||
let (start_buffer, start) =
|
||||
buffer.text_anchor_for_position(range.start, cx)?;
|
||||
let (end_buffer, end) =
|
||||
buffer.text_anchor_for_position(range.end, cx)?;
|
||||
if start_buffer != end_buffer {
|
||||
return None;
|
||||
}
|
||||
Some((start_buffer, start..end))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (buffer, range) in selection_ranges {
|
||||
store.add_excerpt(range, buffer, cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if let Some(context_editor) = panel.active_context_editor() {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,6 +425,8 @@ impl CodegenAlternative {
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
|
||||
@@ -4,6 +4,7 @@ use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
@@ -23,6 +24,7 @@ pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
Symbol,
|
||||
Excerpt,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
@@ -33,6 +35,7 @@ impl ContextKind {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::Symbol => IconName::Code,
|
||||
ContextKind::Excerpt => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
@@ -46,6 +49,7 @@ pub enum AssistantContext {
|
||||
Symbol(SymbolContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
Excerpt(ExcerptContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
@@ -56,6 +60,7 @@ impl AssistantContext {
|
||||
Self::Symbol(symbol) => symbol.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
Self::Excerpt(excerpt) => excerpt.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +160,14 @@ pub struct ContextSymbolId {
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExcerptContext {
|
||||
pub id: ContextId,
|
||||
pub range: Range<Anchor>,
|
||||
pub line_range: Range<Point>,
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
/// Formats a collection of contexts into a string representation
|
||||
pub fn format_context_as_string<'a>(
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
@@ -163,6 +176,7 @@ pub fn format_context_as_string<'a>(
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
let mut symbol_context = Vec::new();
|
||||
let mut excerpt_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
|
||||
@@ -171,6 +185,7 @@ pub fn format_context_as_string<'a>(
|
||||
AssistantContext::File(context) => file_context.push(context),
|
||||
AssistantContext::Directory(context) => directory_context.push(context),
|
||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||
AssistantContext::Excerpt(context) => excerpt_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
}
|
||||
@@ -179,6 +194,7 @@ pub fn format_context_as_string<'a>(
|
||||
if file_context.is_empty()
|
||||
&& directory_context.is_empty()
|
||||
&& symbol_context.is_empty()
|
||||
&& excerpt_context.is_empty()
|
||||
&& fetch_context.is_empty()
|
||||
&& thread_context.is_empty()
|
||||
{
|
||||
@@ -216,6 +232,15 @@ pub fn format_context_as_string<'a>(
|
||||
result.push_str("</symbols>\n");
|
||||
}
|
||||
|
||||
if !excerpt_context.is_empty() {
|
||||
result.push_str("<excerpts>\n");
|
||||
for context in excerpt_context {
|
||||
result.push_str(&context.context_buffer.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</excerpts>\n");
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
result.push_str("<fetched_urls>\n");
|
||||
for context in &fetch_context {
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::sync::atomic::AtomicBool;
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
@@ -37,7 +38,24 @@ pub(crate) enum Match {
|
||||
File(FileMatch),
|
||||
Thread(ThreadMatch),
|
||||
Fetch(SharedString),
|
||||
Mode(ContextPickerMode),
|
||||
Mode(ModeMatch),
|
||||
}
|
||||
|
||||
pub struct ModeMatch {
|
||||
mat: Option<StringMatch>,
|
||||
mode: ContextPickerMode,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
pub fn score(&self) -> f64 {
|
||||
match self {
|
||||
Match::File(file) => file.mat.score,
|
||||
Match::Mode(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.),
|
||||
Match::Thread(_) => 1.,
|
||||
Match::Symbol(_) => 1.,
|
||||
Match::Fetch(_) => 1.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn search(
|
||||
@@ -126,19 +144,54 @@ fn search(
|
||||
matches.extend(
|
||||
supported_context_picker_modes(&thread_store)
|
||||
.into_iter()
|
||||
.map(Match::Mode),
|
||||
.map(|mode| Match::Mode(ModeMatch { mode, mat: None })),
|
||||
);
|
||||
|
||||
Task::ready(matches)
|
||||
} else {
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
let search_files_task =
|
||||
search_files(query.clone(), cancellation_flag.clone(), &workspace, cx);
|
||||
|
||||
let modes = supported_context_picker_modes(&thread_store);
|
||||
let mode_candidates = modes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, mode)| StringMatchCandidate::new(ix, mode.mention_prefix()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
search_files_task
|
||||
let mut matches = search_files_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(Match::File)
|
||||
.collect()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mode_matches = fuzzy::match_strings(
|
||||
&mode_candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Arc::new(AtomicBool::default()),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches.extend(mode_matches.into_iter().map(|mat| {
|
||||
Match::Mode(ModeMatch {
|
||||
mode: modes[mat.candidate_id],
|
||||
mat: Some(mat),
|
||||
})
|
||||
}));
|
||||
|
||||
matches.sort_by(|a, b| {
|
||||
b.score()
|
||||
.partial_cmp(&a.score())
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
matches
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -548,7 +601,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
context_store.clone(),
|
||||
http_client.clone(),
|
||||
)),
|
||||
Match::Mode(mode) => {
|
||||
Match::Mode(ModeMatch { mode, .. }) => {
|
||||
Some(Self::completion_for_mode(source_range.clone(), mode))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,14 +9,14 @@ use futures::{self, Future, FutureExt, future};
|
||||
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||
use rope::Rope;
|
||||
use rope::{Point, Rope};
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
use util::{ResultExt as _, maybe};
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
ExcerptContext, FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -110,7 +110,7 @@ impl ContextStore {
|
||||
}
|
||||
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -129,7 +129,7 @@ impl ContextStore {
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
@@ -206,7 +206,7 @@ impl ContextStore {
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
for buffer in buffers.into_iter().flatten() {
|
||||
if let Some((buffer_info, text_task)) =
|
||||
collect_buffer_info_and_text(buffer, None, cx).log_err()
|
||||
collect_buffer_info_and_text(buffer, cx).log_err()
|
||||
{
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
@@ -290,11 +290,14 @@ impl ContextStore {
|
||||
}
|
||||
}
|
||||
|
||||
let (buffer_info, collect_content_task) =
|
||||
match collect_buffer_info_and_text(buffer, Some(symbol_enclosing_range.clone()), cx) {
|
||||
Ok((buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let (buffer_info, collect_content_task) = match collect_buffer_info_and_text_for_range(
|
||||
buffer,
|
||||
symbol_enclosing_range.clone(),
|
||||
cx,
|
||||
) {
|
||||
Ok((_, buffer_info, collect_context_task)) => (buffer_info, collect_context_task),
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let content = collect_content_task.await;
|
||||
@@ -416,6 +419,49 @@ impl ContextStore {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_excerpt(
|
||||
&mut self,
|
||||
range: Range<Anchor>,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (line_range, buffer_info, text_task) = this.update(cx, |_, cx| {
|
||||
collect_buffer_info_and_text_for_range(buffer, range.clone(), cx)
|
||||
})??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_excerpt(
|
||||
make_context_buffer(buffer_info, text),
|
||||
range,
|
||||
line_range,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_excerpt(
|
||||
&mut self,
|
||||
context_buffer: ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
line_range: Range<Point>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.context.push(AssistantContext::Excerpt(ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn accept_suggested_context(
|
||||
&mut self,
|
||||
suggested: &SuggestedContext,
|
||||
@@ -465,6 +511,7 @@ impl ContextStore {
|
||||
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||
self.symbols.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::Excerpt(_) => {}
|
||||
AssistantContext::FetchedUrl(_) => {
|
||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
@@ -592,6 +639,7 @@ impl ContextStore {
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
| AssistantContext::Excerpt(_)
|
||||
| AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_) => None,
|
||||
})
|
||||
@@ -643,41 +691,78 @@ fn make_context_symbol(
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text_for_range(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Result<(Range<Point>, BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer
|
||||
.read(cx)
|
||||
.text_for_range(range.clone())
|
||||
.collect::<Rope>();
|
||||
|
||||
let line_range = range.to_point(&buffer.read(cx).snapshot());
|
||||
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task = cx.background_spawn({
|
||||
let line_range = line_range.clone();
|
||||
async move { to_fenced_codeblock(&full_path, content, Some(line_range)) }
|
||||
});
|
||||
|
||||
Ok((line_range, buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Option<Range<Anchor>>,
|
||||
cx: &App,
|
||||
) -> Result<(BufferInfo, Task<SharedString>)> {
|
||||
let content = buffer.read(cx).as_rope().clone();
|
||||
|
||||
let buffer_info = collect_buffer_info(buffer, cx)?;
|
||||
let full_path = buffer_info.file.full_path(cx);
|
||||
|
||||
let text_task =
|
||||
cx.background_spawn(async move { to_fenced_codeblock(&full_path, content, None) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
}
|
||||
|
||||
fn collect_buffer_info(buffer: Entity<Buffer>, cx: &App) -> Result<BufferInfo> {
|
||||
let buffer_ref = buffer.read(cx);
|
||||
let file = buffer_ref.file().context("file context must have a path")?;
|
||||
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
let version = buffer_ref.version();
|
||||
let content = if let Some(range) = range {
|
||||
buffer_ref.text_for_range(range).collect::<Rope>()
|
||||
} else {
|
||||
buffer_ref.as_rope().clone()
|
||||
};
|
||||
|
||||
let buffer_info = BufferInfo {
|
||||
Ok(BufferInfo {
|
||||
buffer,
|
||||
id: buffer_ref.remote_id(),
|
||||
file: file.clone(),
|
||||
version,
|
||||
};
|
||||
|
||||
let full_path = file.full_path(cx);
|
||||
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&full_path, content) });
|
||||
|
||||
Ok((buffer_info, text_task))
|
||||
})
|
||||
}
|
||||
|
||||
fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
fn to_fenced_codeblock(
|
||||
path: &Path,
|
||||
content: Rope,
|
||||
line_range: Option<Range<Point>>,
|
||||
) -> SharedString {
|
||||
let line_range_text = line_range.map(|range| {
|
||||
if range.start.row == range.end.row {
|
||||
format!(":{}", range.start.row + 1)
|
||||
} else {
|
||||
format!(":{}-{}", range.start.row + 1, range.end.row + 1)
|
||||
}
|
||||
});
|
||||
|
||||
let path_extension = path.extension().and_then(|ext| ext.to_str());
|
||||
let path_string = path.to_string_lossy();
|
||||
let capacity = 3
|
||||
+ path_extension.map_or(0, |extension| extension.len() + 1)
|
||||
+ path_string.len()
|
||||
+ line_range_text.as_ref().map_or(0, |text| text.len())
|
||||
+ 1
|
||||
+ content.len()
|
||||
+ 5;
|
||||
@@ -691,6 +776,10 @@ fn to_fenced_codeblock(path: &Path, content: Rope) -> SharedString {
|
||||
}
|
||||
buffer.push_str(&path_string);
|
||||
|
||||
if let Some(line_range_text) = line_range_text {
|
||||
buffer.push_str(&line_range_text);
|
||||
}
|
||||
|
||||
buffer.push('\n');
|
||||
for chunk in content.chunks() {
|
||||
buffer.push_str(&chunk);
|
||||
@@ -769,6 +858,14 @@ pub fn refresh_context_store_text(
|
||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&excerpt_context.context_buffer.buffer)
|
||||
{
|
||||
let context_store = context_store.clone();
|
||||
return refresh_excerpt_text(context_store, excerpt_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Thread(thread_context) => {
|
||||
if changed_buffers.is_empty() {
|
||||
let context_store = context_store.clone();
|
||||
@@ -880,6 +977,34 @@ fn refresh_symbol_text(
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_excerpt_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
excerpt_context: &ExcerptContext,
|
||||
cx: &App,
|
||||
) -> Option<Task<()>> {
|
||||
let id = excerpt_context.id;
|
||||
let range = excerpt_context.range.clone();
|
||||
let task = refresh_context_excerpt(&excerpt_context.context_buffer, range.clone(), cx);
|
||||
if let Some(task) = task {
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let (line_range, context_buffer) = task.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_excerpt_context = ExcerptContext {
|
||||
id,
|
||||
range,
|
||||
line_range,
|
||||
context_buffer,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Excerpt(new_excerpt_context));
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_thread_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_context: &ThreadContext,
|
||||
@@ -908,13 +1033,29 @@ fn refresh_context_buffer(
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), cx).log_err()?;
|
||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_excerpt(
|
||||
context_buffer: &ContextBuffer,
|
||||
range: Range<Anchor>,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = (Range<Point>, ContextBuffer)> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (line_range, buffer_info, text_task) =
|
||||
collect_buffer_info_and_text_for_range(context_buffer.buffer.clone(), range, cx)
|
||||
.log_err()?;
|
||||
Some(text_task.map(move |text| (line_range, make_context_buffer(buffer_info, text))))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_symbol(
|
||||
context_symbol: &ContextSymbol,
|
||||
cx: &App,
|
||||
@@ -922,9 +1063,9 @@ fn refresh_context_symbol(
|
||||
let buffer = context_symbol.buffer.read(cx);
|
||||
let project_path = buffer.project_path(cx)?;
|
||||
if buffer.version.changed_since(&context_symbol.buffer_version) {
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
let (_, buffer_info, text_task) = collect_buffer_info_and_text_for_range(
|
||||
context_symbol.buffer.clone(),
|
||||
Some(context_symbol.enclosing_range.clone()),
|
||||
context_symbol.enclosing_range.clone(),
|
||||
cx,
|
||||
)
|
||||
.log_err()?;
|
||||
|
||||
@@ -2,22 +2,23 @@ use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use crate::context::{AssistantContext, format_context_as_string};
|
||||
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, EditorStyle,
|
||||
MultiBuffer,
|
||||
ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, MultiBuffer,
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, Entity, Focusable, Subscription, TextStyle, WeakEntity,
|
||||
linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
@@ -55,6 +56,8 @@ pub struct MessageEditor {
|
||||
edits_expanded: bool,
|
||||
editor_is_expanded: bool,
|
||||
waiting_for_summaries_to_send: bool,
|
||||
last_estimated_token_count: Option<usize>,
|
||||
update_token_count_task: Option<Task<anyhow::Result<()>>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -129,8 +132,18 @@ impl MessageEditor {
|
||||
let incompatible_tools =
|
||||
cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
|
||||
|
||||
let subscriptions =
|
||||
vec![cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event)];
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||
cx.subscribe(&editor, |this, _, event, cx| match event {
|
||||
EditorEvent::BufferEdited => {
|
||||
this.message_or_context_changed(true, cx);
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
cx.observe(&context_store, |this, _, cx| {
|
||||
this.message_or_context_changed(false, cx);
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
@@ -156,6 +169,8 @@ impl MessageEditor {
|
||||
waiting_for_summaries_to_send: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
last_estimated_token_count: None,
|
||||
update_token_count_task: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -256,6 +271,9 @@ impl MessageEditor {
|
||||
text
|
||||
});
|
||||
|
||||
self.last_estimated_token_count.take();
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
|
||||
let refresh_task =
|
||||
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
|
||||
|
||||
@@ -275,6 +293,21 @@ impl MessageEditor {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let excerpt_ids = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.filter(|ctx| matches!(ctx, AssistantContext::Excerpt(_)))
|
||||
.map(|ctx| ctx.id())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for id in excerpt_ids {
|
||||
context_store.remove_context(id, cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.log_err()
|
||||
@@ -297,6 +330,7 @@ impl MessageEditor {
|
||||
// Send to model after summaries are done
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(model, request_kind, cx);
|
||||
})
|
||||
.log_err();
|
||||
@@ -937,6 +971,82 @@ impl MessageEditor {
|
||||
.label_size(LabelSize::Small),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn last_estimated_token_count(&self) -> Option<usize> {
|
||||
self.last_estimated_token_count
|
||||
}
|
||||
|
||||
pub fn is_waiting_to_update_token_count(&self) -> bool {
|
||||
self.update_token_count_task.is_some()
|
||||
}
|
||||
|
||||
fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
|
||||
cx.emit(MessageEditorEvent::Changed);
|
||||
self.update_token_count_task.take();
|
||||
|
||||
let Some(default_model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
self.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
|
||||
let context_store = self.context_store.clone();
|
||||
let editor = self.editor.clone();
|
||||
let thread = self.thread.clone();
|
||||
|
||||
self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
|
||||
if debounce {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(200))
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = if let Some(task) = cx.update(|cx| {
|
||||
let context = context_store.read(cx).context().iter();
|
||||
let new_context = thread.read(cx).filter_new_context(context);
|
||||
let context_text =
|
||||
format_context_as_string(new_context, cx).unwrap_or(String::new());
|
||||
let message_text = editor.read(cx).text(cx);
|
||||
|
||||
let content = context_text + &message_text;
|
||||
|
||||
if content.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let request = language_model::LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: language_model::Role::User,
|
||||
content: vec![content.into()],
|
||||
cache: false,
|
||||
}],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(default_model.model.count_tokens(request, cx))
|
||||
})? {
|
||||
task.await?
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_estimated_token_count = Some(token_count);
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
this.update_token_count_task.take();
|
||||
})
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
pub enum MessageEditorEvent {
|
||||
EstimatedTokenCount,
|
||||
Changed,
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
@@ -949,6 +1059,7 @@ impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let thread = self.thread.read(cx);
|
||||
let total_token_usage = thread.total_token_usage(cx);
|
||||
let token_usage_ratio = total_token_usage.ratio();
|
||||
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
@@ -997,15 +1108,8 @@ impl Render for MessageEditor {
|
||||
parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
|
||||
})
|
||||
.child(self.render_editor(font_size, line_height, window, cx))
|
||||
.when(
|
||||
total_token_usage.ratio != TokenUsageRatio::Normal,
|
||||
|parent| {
|
||||
parent.child(self.render_token_limit_callout(
|
||||
line_height,
|
||||
total_token_usage.ratio,
|
||||
cx,
|
||||
))
|
||||
},
|
||||
)
|
||||
.when(token_usage_ratio != TokenUsageRatio::Normal, |parent| {
|
||||
parent.child(self.render_token_limit_callout(line_height, token_usage_ratio, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,8 @@ impl TerminalInlineAssistant {
|
||||
request_message.content.push(prompt.into());
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -4,9 +4,9 @@ use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
@@ -18,12 +18,14 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,
|
||||
LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, TokenUsage,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent,
|
||||
ModelRequestLimitReachedError, PaymentRequiredError, RequestUsage, Role, StopReason,
|
||||
TokenUsage,
|
||||
};
|
||||
use project::Project;
|
||||
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
|
||||
use prompt_store::PromptBuilder;
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -68,6 +70,24 @@ impl From<&str> for ThreadId {
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of the user prompt that initiated a request.
|
||||
///
|
||||
/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key).
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct PromptId(Arc<str>);
|
||||
|
||||
impl PromptId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4().to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PromptId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct MessageId(pub(crate) usize);
|
||||
|
||||
@@ -226,7 +246,33 @@ pub enum DetailedSummaryState {
|
||||
pub struct TotalTokenUsage {
|
||||
pub total: usize,
|
||||
pub max: usize,
|
||||
pub ratio: TokenUsageRatio,
|
||||
}
|
||||
|
||||
impl TotalTokenUsage {
|
||||
pub fn ratio(&self) -> TokenUsageRatio {
|
||||
#[cfg(debug_assertions)]
|
||||
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
|
||||
.unwrap_or("0.8".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let warning_threshold: f32 = 0.8;
|
||||
|
||||
if self.total >= self.max {
|
||||
TokenUsageRatio::Exceeded
|
||||
} else if self.total as f32 / self.max as f32 >= warning_threshold {
|
||||
TokenUsageRatio::Warning
|
||||
} else {
|
||||
TokenUsageRatio::Normal
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&self, tokens: usize) -> TotalTokenUsage {
|
||||
TotalTokenUsage {
|
||||
total: self.total + tokens,
|
||||
max: self.max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
@@ -246,6 +292,7 @@ pub struct Thread {
|
||||
detailed_summary_state: DetailedSummaryState,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
last_prompt_id: PromptId,
|
||||
context: BTreeMap<ContextId, AssistantContext>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
project_context: SharedProjectContext,
|
||||
@@ -260,6 +307,7 @@ pub struct Thread {
|
||||
last_restore_checkpoint: Option<LastRestoreCheckpoint>,
|
||||
pending_checkpoint: Option<ThreadCheckpoint>,
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
request_token_usage: Vec<TokenUsage>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
exceeded_window_error: Option<ExceededWindowError>,
|
||||
feedback: Option<ThreadFeedback>,
|
||||
@@ -291,6 +339,7 @@ impl Thread {
|
||||
detailed_summary_state: DetailedSummaryState::NotGenerated,
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
last_prompt_id: PromptId::new(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
project_context: system_prompt,
|
||||
@@ -310,6 +359,7 @@ impl Thread {
|
||||
.spawn(async move { Some(project_snapshot.await) })
|
||||
.shared()
|
||||
},
|
||||
request_token_usage: Vec::new(),
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
exceeded_window_error: None,
|
||||
feedback: None,
|
||||
@@ -363,6 +413,7 @@ impl Thread {
|
||||
})
|
||||
.collect(),
|
||||
next_message_id,
|
||||
last_prompt_id: PromptId::new(),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
project_context,
|
||||
@@ -377,6 +428,7 @@ impl Thread {
|
||||
tool_use,
|
||||
action_log: cx.new(|_| ActionLog::new(project)),
|
||||
initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
|
||||
request_token_usage: serialized.request_token_usage,
|
||||
cumulative_token_usage: serialized.cumulative_token_usage,
|
||||
exceeded_window_error: None,
|
||||
feedback: None,
|
||||
@@ -401,6 +453,10 @@ impl Thread {
|
||||
self.updated_at = Utc::now();
|
||||
}
|
||||
|
||||
pub fn advance_prompt_id(&mut self) {
|
||||
self.last_prompt_id = PromptId::new();
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Option<SharedString> {
|
||||
self.summary.clone()
|
||||
}
|
||||
@@ -630,10 +686,30 @@ impl Thread {
|
||||
self.tool_use.tool_result(id)
|
||||
}
|
||||
|
||||
pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc<str>> {
|
||||
Some(&self.tool_use.tool_result(id)?.content)
|
||||
}
|
||||
|
||||
pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option<AnyToolCard> {
|
||||
self.tool_use.tool_result_card(id).cloned()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
|
||||
self.tool_use.message_has_tool_results(message_id)
|
||||
}
|
||||
|
||||
/// Filter out contexts that have already been included in previous messages
|
||||
pub fn filter_new_context<'a>(
|
||||
&self,
|
||||
context: impl Iterator<Item = &'a AssistantContext>,
|
||||
) -> impl Iterator<Item = &'a AssistantContext> {
|
||||
context.filter(|ctx| self.is_context_new(ctx))
|
||||
}
|
||||
|
||||
fn is_context_new(&self, context: &AssistantContext) -> bool {
|
||||
!self.context.contains_key(&context.id())
|
||||
}
|
||||
|
||||
pub fn insert_user_message(
|
||||
&mut self,
|
||||
text: impl Into<String>,
|
||||
@@ -645,10 +721,9 @@ impl Thread {
|
||||
|
||||
let message_id = self.insert_message(Role::User, vec![MessageSegment::Text(text)], cx);
|
||||
|
||||
// Filter out contexts that have already been included in previous messages
|
||||
let new_context: Vec<_> = context
|
||||
.into_iter()
|
||||
.filter(|ctx| !self.context.contains_key(&ctx.id()))
|
||||
.filter(|ctx| self.is_context_new(ctx))
|
||||
.collect();
|
||||
|
||||
if !new_context.is_empty() {
|
||||
@@ -676,6 +751,12 @@ impl Thread {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
log.buffer_added_as_context(
|
||||
excerpt_context.context_buffer.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_) | AssistantContext::Thread(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -828,6 +909,7 @@ impl Thread {
|
||||
.collect(),
|
||||
initial_project_snapshot,
|
||||
cumulative_token_usage: this.cumulative_token_usage,
|
||||
request_token_usage: this.request_token_usage.clone(),
|
||||
detailed_summary_state: this.detailed_summary_state.clone(),
|
||||
exceeded_window_error: this.exceeded_window_error.clone(),
|
||||
})
|
||||
@@ -882,9 +964,11 @@ impl Thread {
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
request_kind: RequestKind,
|
||||
cx: &App,
|
||||
cx: &mut Context<Self>,
|
||||
) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: Some(self.id.to_string()),
|
||||
prompt_id: Some(self.last_prompt_id.to_string()),
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
@@ -892,20 +976,33 @@ impl Thread {
|
||||
};
|
||||
|
||||
if let Some(project_context) = self.project_context.borrow().as_ref() {
|
||||
if let Some(system_prompt) = self
|
||||
match self
|
||||
.prompt_builder
|
||||
.generate_assistant_system_prompt(project_context)
|
||||
.context("failed to generate assistant system prompt")
|
||||
.log_err()
|
||||
{
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
Err(err) => {
|
||||
let message = format!("{err:?}").into();
|
||||
log::error!("{message}");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error generating system prompt".into(),
|
||||
message,
|
||||
}));
|
||||
}
|
||||
Ok(system_prompt) => {
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("project_context not set.")
|
||||
let message = "Context for system prompt unexpectedly not ready.".into();
|
||||
log::error!("{message}");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error generating system prompt".into(),
|
||||
message,
|
||||
}));
|
||||
}
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -1013,16 +1110,24 @@ impl Thread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let pending_completion_id = post_inc(&mut self.completion_count);
|
||||
|
||||
let prompt_id = self.last_prompt_id.clone();
|
||||
let task = cx.spawn(async move |thread, cx| {
|
||||
let stream = model.stream_completion(request, &cx);
|
||||
let stream_completion_future = model.stream_completion_with_usage(request, &cx);
|
||||
let initial_token_usage =
|
||||
thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage);
|
||||
let stream_completion = async {
|
||||
let mut events = stream.await?;
|
||||
let (mut events, usage) = stream_completion_future.await?;
|
||||
let mut stop_reason = StopReason::EndTurn;
|
||||
let mut current_token_usage = TokenUsage::default();
|
||||
|
||||
if let Some(usage) = usage {
|
||||
thread
|
||||
.update(cx, |_thread, cx| {
|
||||
cx.emit(ThreadEvent::UsageUpdated(usage));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
let event = event?;
|
||||
|
||||
@@ -1039,6 +1144,7 @@ impl Thread {
|
||||
stop_reason = reason;
|
||||
}
|
||||
LanguageModelCompletionEvent::UsageUpdate(token_usage) => {
|
||||
thread.update_token_usage_at_last_message(token_usage);
|
||||
thread.cumulative_token_usage = thread.cumulative_token_usage
|
||||
+ token_usage
|
||||
- current_token_usage;
|
||||
@@ -1150,6 +1256,12 @@ impl Thread {
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::MaxMonthlySpendReached,
|
||||
));
|
||||
} else if let Some(error) =
|
||||
error.downcast_ref::<ModelRequestLimitReachedError>()
|
||||
{
|
||||
cx.emit(ThreadEvent::ShowError(
|
||||
ThreadError::ModelRequestLimitReached { plan: error.plan },
|
||||
));
|
||||
} else if let Some(known_error) =
|
||||
error.downcast_ref::<LanguageModelKnownError>()
|
||||
{
|
||||
@@ -1189,6 +1301,7 @@ impl Thread {
|
||||
telemetry::event!(
|
||||
"Assistant Thread Completion",
|
||||
thread_id = thread.id().to_string(),
|
||||
prompt_id = prompt_id,
|
||||
model = model.telemetry_id(),
|
||||
model_provider = model.provider_id().to_string(),
|
||||
input_tokens = usage.input_tokens,
|
||||
@@ -1231,8 +1344,15 @@ impl Thread {
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
let stream = model.model.stream_completion_text_with_usage(request, &cx);
|
||||
let (mut messages, usage) = stream.await?;
|
||||
|
||||
if let Some(usage) = usage {
|
||||
this.update(cx, |_thread, cx| {
|
||||
cx.emit(ThreadEvent::UsageUpdated(usage));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
let mut new_summary = String::new();
|
||||
while let Some(message) = messages.stream.next().await {
|
||||
@@ -1419,6 +1539,12 @@ impl Thread {
|
||||
)
|
||||
};
|
||||
|
||||
// Store the card separately if it exists
|
||||
if let Some(card) = tool_result.card.clone() {
|
||||
self.tool_use
|
||||
.insert_tool_result_card(tool_use_id.clone(), card);
|
||||
}
|
||||
|
||||
cx.spawn({
|
||||
async move |thread: WeakEntity<Thread>, cx| {
|
||||
let output = tool_result.output.await;
|
||||
@@ -1868,6 +1994,35 @@ impl Thread {
|
||||
self.cumulative_token_usage
|
||||
}
|
||||
|
||||
pub fn token_usage_up_to_message(&self, message_id: MessageId, cx: &App) -> TotalTokenUsage {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return TotalTokenUsage::default();
|
||||
};
|
||||
|
||||
let max = model.model.max_token_count();
|
||||
|
||||
let index = self
|
||||
.messages
|
||||
.iter()
|
||||
.position(|msg| msg.id == message_id)
|
||||
.unwrap_or(0);
|
||||
|
||||
if index == 0 {
|
||||
return TotalTokenUsage { total: 0, max };
|
||||
}
|
||||
|
||||
let token_usage = &self
|
||||
.request_token_usage
|
||||
.get(index - 1)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
TotalTokenUsage {
|
||||
total: token_usage.total_tokens() as usize,
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn total_token_usage(&self, cx: &App) -> TotalTokenUsage {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.default_model() else {
|
||||
@@ -1881,30 +2036,33 @@ impl Thread {
|
||||
return TotalTokenUsage {
|
||||
total: exceeded_error.token_count,
|
||||
max,
|
||||
ratio: TokenUsageRatio::Exceeded,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD")
|
||||
.unwrap_or("0.8".to_string())
|
||||
.parse()
|
||||
.unwrap();
|
||||
#[cfg(not(debug_assertions))]
|
||||
let warning_threshold: f32 = 0.8;
|
||||
let total = self
|
||||
.token_usage_at_last_message()
|
||||
.unwrap_or_default()
|
||||
.total_tokens() as usize;
|
||||
|
||||
let total = self.cumulative_token_usage.total_tokens() as usize;
|
||||
TotalTokenUsage { total, max }
|
||||
}
|
||||
|
||||
let ratio = if total >= max {
|
||||
TokenUsageRatio::Exceeded
|
||||
} else if total as f32 / max as f32 >= warning_threshold {
|
||||
TokenUsageRatio::Warning
|
||||
} else {
|
||||
TokenUsageRatio::Normal
|
||||
};
|
||||
fn token_usage_at_last_message(&self) -> Option<TokenUsage> {
|
||||
self.request_token_usage
|
||||
.get(self.messages.len().saturating_sub(1))
|
||||
.or_else(|| self.request_token_usage.last())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
TotalTokenUsage { total, max, ratio }
|
||||
fn update_token_usage_at_last_message(&mut self, token_usage: TokenUsage) {
|
||||
let placeholder = self.token_usage_at_last_message().unwrap_or_default();
|
||||
self.request_token_usage
|
||||
.resize(self.messages.len(), placeholder);
|
||||
|
||||
if let Some(last) = self.request_token_usage.last_mut() {
|
||||
*last = token_usage;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deny_tool_use(
|
||||
@@ -1929,6 +2087,8 @@ pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
#[error("Max monthly spend reached")]
|
||||
MaxMonthlySpendReached,
|
||||
#[error("Model request limit reached")]
|
||||
ModelRequestLimitReached { plan: Plan },
|
||||
#[error("Message {header}: {message}")]
|
||||
Message {
|
||||
header: SharedString,
|
||||
@@ -1939,6 +2099,7 @@ pub enum ThreadError {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThreadEvent {
|
||||
ShowError(ThreadError),
|
||||
UsageUpdated(RequestUsage),
|
||||
StreamedCompletion,
|
||||
StreamedAssistantText(MessageId, String),
|
||||
StreamedAssistantThinking(MessageId, String),
|
||||
@@ -2044,7 +2205,7 @@ fn main() {{
|
||||
assert_eq!(message.context, expected_context);
|
||||
|
||||
// Check message in request
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
@@ -2136,7 +2297,7 @@ fn main() {{
|
||||
assert!(message3.context.contains("file3.rs"));
|
||||
|
||||
// Check entire request to make sure all contexts are properly included
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
@@ -2188,7 +2349,7 @@ fn main() {{
|
||||
assert_eq!(message.context, "");
|
||||
|
||||
// Check message in request
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
@@ -2208,7 +2369,7 @@ fn main() {{
|
||||
assert_eq!(message2.context, "");
|
||||
|
||||
// Check that both messages appear in the request
|
||||
let request = thread.read_with(cx, |thread, cx| {
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
@@ -2250,7 +2411,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a request and check that it doesn't have a stale buffer warning yet
|
||||
let initial_request = thread.read_with(cx, |thread, cx| {
|
||||
let initial_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
@@ -2280,7 +2441,7 @@ fn main() {{
|
||||
});
|
||||
|
||||
// Create a new request and check for the stale buffer warning
|
||||
let new_request = thread.read_with(cx, |thread, cx| {
|
||||
let new_request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(RequestKind::Chat, cx)
|
||||
});
|
||||
|
||||
@@ -2309,6 +2470,7 @@ fn main() {{
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
AssistantSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
@@ -2348,7 +2510,8 @@ fn main() {{
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
|
||||
|
||||
@@ -12,8 +12,9 @@ use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{
|
||||
App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString,
|
||||
Subscription, Task, prelude::*,
|
||||
@@ -22,7 +23,10 @@ use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::{Project, Worktree};
|
||||
use prompt_store::{ProjectContext, PromptBuilder, RulesFileContext, WorktreeContext};
|
||||
use prompt_store::{
|
||||
DefaultUserRulesContext, ProjectContext, PromptBuilder, PromptId, PromptStore,
|
||||
PromptsUpdatedEvent, RulesFileContext, WorktreeContext,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::ResultExt as _;
|
||||
@@ -62,6 +66,8 @@ pub struct ThreadStore {
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<SerializedThreadMetadata>,
|
||||
project_context: SharedProjectContext,
|
||||
reload_system_prompt_tx: mpsc::Sender<()>,
|
||||
_reload_system_prompt_task: Task<()>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -77,12 +83,22 @@ impl ThreadStore {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
cx: &mut App,
|
||||
) -> Task<Entity<Self>> {
|
||||
let thread_store = cx.new(|cx| Self::new(project, tools, prompt_builder, cx));
|
||||
let reload = thread_store.update(cx, |store, cx| store.reload_system_prompt(cx));
|
||||
cx.foreground_executor().spawn(async move {
|
||||
reload.await;
|
||||
thread_store
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let prompt_store = PromptStore::global(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let prompt_store = prompt_store.await.ok();
|
||||
let (thread_store, ready_rx) = cx.update(|cx| {
|
||||
let mut option_ready_rx = None;
|
||||
let thread_store = cx.new(|cx| {
|
||||
let (thread_store, ready_rx) =
|
||||
Self::new(project, tools, prompt_builder, prompt_store, cx);
|
||||
option_ready_rx = Some(ready_rx);
|
||||
thread_store
|
||||
});
|
||||
(thread_store, option_ready_rx.take().unwrap())
|
||||
})?;
|
||||
ready_rx.await?;
|
||||
Ok(thread_store)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,17 +106,53 @@ impl ThreadStore {
|
||||
project: Entity<Project>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
) -> (Self, oneshot::Receiver<()>) {
|
||||
let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let settings_subscription =
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||
this.load_default_profile(cx);
|
||||
});
|
||||
let project_subscription = cx.subscribe(&project, Self::handle_project_event);
|
||||
}),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
];
|
||||
|
||||
if let Some(prompt_store) = prompt_store.as_ref() {
|
||||
subscriptions.push(cx.subscribe(
|
||||
prompt_store,
|
||||
|this, _prompt_store, PromptsUpdatedEvent, _cx| {
|
||||
this.enqueue_system_prompt_reload();
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// This channel and task prevent concurrent and redundant loading of the system prompt.
|
||||
let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1);
|
||||
let (ready_tx, ready_rx) = oneshot::channel();
|
||||
let mut ready_tx = Some(ready_tx);
|
||||
let reload_system_prompt_task = cx.spawn({
|
||||
async move |thread_store, cx| {
|
||||
loop {
|
||||
let Some(reload_task) = thread_store
|
||||
.update(cx, |thread_store, cx| {
|
||||
thread_store.reload_system_prompt(prompt_store.clone(), cx)
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
reload_task.await;
|
||||
if let Some(ready_tx) = ready_tx.take() {
|
||||
ready_tx.send(()).ok();
|
||||
}
|
||||
reload_system_prompt_rx.next().await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
@@ -110,23 +162,25 @@ impl ThreadStore {
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
project_context: SharedProjectContext::default(),
|
||||
_subscriptions: vec![settings_subscription, project_subscription],
|
||||
reload_system_prompt_tx,
|
||||
_reload_system_prompt_task: reload_system_prompt_task,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
(this, ready_rx)
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut Context<Self>,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => {
|
||||
self.reload_system_prompt(cx).detach();
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
project::Event::WorktreeUpdatedEntries(_, items) => {
|
||||
if items.iter().any(|(path, _, _)| {
|
||||
@@ -134,16 +188,25 @@ impl ThreadStore {
|
||||
.iter()
|
||||
.any(|name| path.as_ref() == Path::new(name))
|
||||
}) {
|
||||
self.reload_system_prompt(cx).detach();
|
||||
self.enqueue_system_prompt_reload();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reload_system_prompt(&self, cx: &mut Context<Self>) -> Task<()> {
|
||||
fn enqueue_system_prompt_reload(&mut self) {
|
||||
self.reload_system_prompt_tx.try_send(()).ok();
|
||||
}
|
||||
|
||||
// Note that this should only be called from `reload_system_prompt_task`.
|
||||
fn reload_system_prompt(
|
||||
&self,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
let project = self.project.read(cx);
|
||||
let tasks = project
|
||||
let worktree_tasks = project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
Self::load_worktree_info_for_system_prompt(
|
||||
@@ -153,10 +216,23 @@ impl ThreadStore {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let default_user_rules_task = match prompt_store {
|
||||
None => Task::ready(vec![]),
|
||||
Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| {
|
||||
let prompts = prompt_store.default_prompt_metadata();
|
||||
let load_tasks = prompts.into_iter().map(|prompt_metadata| {
|
||||
let contents = prompt_store.load(prompt_metadata.id, cx);
|
||||
async move { (contents.await, prompt_metadata) }
|
||||
});
|
||||
cx.background_spawn(future::join_all(load_tasks))
|
||||
}),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let worktrees = results
|
||||
let (worktrees, default_user_rules) =
|
||||
future::join(future::join_all(worktree_tasks), default_user_rules_task).await;
|
||||
|
||||
let worktrees = worktrees
|
||||
.into_iter()
|
||||
.map(|(worktree, rules_error)| {
|
||||
if let Some(rules_error) = rules_error {
|
||||
@@ -165,8 +241,33 @@ impl ThreadStore {
|
||||
worktree
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let default_user_rules = default_user_rules
|
||||
.into_iter()
|
||||
.flat_map(|(contents, prompt_metadata)| match contents {
|
||||
Ok(contents) => Some(DefaultUserRulesContext {
|
||||
uuid: match prompt_metadata.id {
|
||||
PromptId::User { uuid } => uuid,
|
||||
PromptId::EditWorkflow => return None,
|
||||
},
|
||||
title: prompt_metadata.title.map(|title| title.to_string()),
|
||||
contents,
|
||||
}),
|
||||
Err(err) => {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(RulesLoadingError {
|
||||
message: format!("{err:?}").into(),
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
*this.project_context.0.borrow_mut() = Some(ProjectContext::new(worktrees));
|
||||
*this.project_context.0.borrow_mut() =
|
||||
Some(ProjectContext::new(worktrees, default_user_rules));
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -509,6 +610,8 @@ pub struct SerializedThread {
|
||||
#[serde(default)]
|
||||
pub cumulative_token_usage: TokenUsage,
|
||||
#[serde(default)]
|
||||
pub request_token_usage: Vec<TokenUsage>,
|
||||
#[serde(default)]
|
||||
pub detailed_summary_state: DetailedSummaryState,
|
||||
#[serde(default)]
|
||||
pub exceeded_window_error: Option<ExceededWindowError>,
|
||||
@@ -597,6 +700,7 @@ impl LegacySerializedThread {
|
||||
messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(),
|
||||
initial_project_snapshot: self.initial_project_snapshot,
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
request_token_usage: Vec::new(),
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::{Tool, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use assistant_tool::{Tool, ToolSource, ToolWorkingSet, ToolWorkingSetEvent};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||
use language_model::{LanguageModel, LanguageModelToolSchemaFormat};
|
||||
@@ -73,7 +73,12 @@ impl Render for IncompatibleToolsTooltip {
|
||||
.children(
|
||||
self.incompatible_tools
|
||||
.iter()
|
||||
.map(|tool| Label::new(tool.name()).size(LabelSize::Small).buffer_font(cx)),
|
||||
.map(|tool| h_flex().gap_4().child(Label::new(tool.name()).size(LabelSize::Small)).map(|parent|
|
||||
match tool.source() {
|
||||
ToolSource::Native => parent,
|
||||
ToolSource::ContextServer { id } => parent.child(Label::new(id).size(LabelSize::Small).color(Color::Muted)),
|
||||
}
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(Label::new("What To Do Instead").size(LabelSize::Small))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{Tool, ToolWorkingSet};
|
||||
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
@@ -27,26 +27,7 @@ pub struct ToolUse {
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
@@ -54,10 +35,9 @@ pub struct ToolUseState {
|
||||
tool_uses_by_user_message: HashMap<MessageId, Vec<LanguageModelToolUseId>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
|
||||
Self {
|
||||
@@ -66,6 +46,7 @@ impl ToolUseState {
|
||||
tool_uses_by_user_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +238,18 @@ impl ToolUseState {
|
||||
self.tool_results.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
|
||||
self.tool_result_cards.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn insert_tool_result_card(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
card: AnyToolCard,
|
||||
) {
|
||||
self.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
|
||||
pub fn request_tool_use(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod agent_notification;
|
||||
mod context_pill;
|
||||
mod user_spending;
|
||||
mod usage_banner;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use context_pill::*;
|
||||
// pub use user_spending::*;
|
||||
pub use usage_banner::*;
|
||||
|
||||
@@ -191,15 +191,12 @@ impl RenderOnce for ContextPill {
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
icon_path: _,
|
||||
kind,
|
||||
kind: _,
|
||||
focused,
|
||||
on_click,
|
||||
} => base_pill
|
||||
.cursor_pointer()
|
||||
.pr_1()
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.border_dashed()
|
||||
.border_color(if *focused {
|
||||
color.border_focused
|
||||
@@ -207,30 +204,17 @@ impl RenderOnce for ContextPill {
|
||||
color.border
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.when(*focused, |this| {
|
||||
this.bg(color.element_background.opacity(0.5))
|
||||
})
|
||||
.child(
|
||||
div().px_0p5().max_w_64().child(
|
||||
div().max_w_64().child(
|
||||
Label::new(name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread
|
||||
| ContextKind::Directory
|
||||
| ContextKind::FetchedUrl
|
||||
| ContextKind::Symbol => "Active",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.size(IconSize::XSmall)
|
||||
.into_any_element(),
|
||||
)
|
||||
.tooltip(|window, cx| {
|
||||
Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx)
|
||||
})
|
||||
@@ -315,6 +299,39 @@ impl AddedContext {
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Excerpt(excerpt_context) => {
|
||||
let full_path = excerpt_context.context_buffer.file.full_path(cx);
|
||||
let mut full_path_string = full_path.to_string_lossy().into_owned();
|
||||
let mut name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
|
||||
let line_range_text = format!(
|
||||
" ({}-{})",
|
||||
excerpt_context.line_range.start.row + 1,
|
||||
excerpt_context.line_range.end.row + 1
|
||||
);
|
||||
|
||||
full_path_string.push_str(&line_range_text);
|
||||
name.push_str(&line_range_text);
|
||||
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
|
||||
AddedContext {
|
||||
id: excerpt_context.id,
|
||||
kind: ContextKind::File, // Use File icon for excerpts
|
||||
name: name.into(),
|
||||
parent,
|
||||
tooltip: Some(full_path_string.into()),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
|
||||
202
crates/agent/src/ui/usage_banner.rs
Normal file
202
crates/agent/src/ui/usage_banner.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use client::zed_urls;
|
||||
use ui::{Banner, ProgressBar, Severity, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageBanner {
|
||||
plan: Plan,
|
||||
requests: i32,
|
||||
}
|
||||
|
||||
impl UsageBanner {
|
||||
pub fn new(plan: Plan, requests: i32) -> Self {
|
||||
Self { plan, requests }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UsageBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let request_limit = self.plan.model_requests_limit();
|
||||
|
||||
let used_percentage = match request_limit {
|
||||
UsageLimit::Limited(limit) => Some((self.requests as f32 / limit as f32) * 100.),
|
||||
UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
let (severity, message) = match request_limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
if self.requests >= limit {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Monthly request limit reached",
|
||||
Plan::ZedProTrial => "Trial request limit reached",
|
||||
Plan::Free => "Free tier request limit reached",
|
||||
};
|
||||
|
||||
(Severity::Error, message)
|
||||
} else if (self.requests as f32 / limit as f32) >= 0.9 {
|
||||
(Severity::Warning, "Approaching request limit")
|
||||
} else {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Zed Pro",
|
||||
Plan::ZedProTrial => "Zed Pro (Trial)",
|
||||
Plan::Free => "Zed Free",
|
||||
};
|
||||
|
||||
(Severity::Info, message)
|
||||
}
|
||||
}
|
||||
UsageLimit::Unlimited => {
|
||||
let message = match self.plan {
|
||||
Plan::ZedPro => "Zed Pro",
|
||||
Plan::ZedProTrial => "Zed Pro (Trial)",
|
||||
Plan::Free => "Zed Free",
|
||||
};
|
||||
|
||||
(Severity::Info, message)
|
||||
}
|
||||
};
|
||||
|
||||
let action = match self.plan {
|
||||
Plan::ZedProTrial | Plan::Free => {
|
||||
Button::new("upgrade", "Upgrade").on_click(|_, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
})
|
||||
}
|
||||
Plan::ZedPro => Button::new("manage", "Manage").on_click(|_, _window, cx| {
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
}),
|
||||
};
|
||||
|
||||
Banner::new().severity(severity).children(
|
||||
h_flex().flex_1().gap_1().child(Label::new(message)).child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.children(used_percentage.map(|percent| {
|
||||
h_flex()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.max_w(px(180.))
|
||||
.child(ProgressBar::new("usage", percent, 100., cx))
|
||||
}))
|
||||
.child(
|
||||
Label::new(match request_limit {
|
||||
UsageLimit::Limited(limit) => {
|
||||
format!("{} / {limit}", self.requests)
|
||||
}
|
||||
UsageLimit::Unlimited => format!("{} / ∞", self.requests),
|
||||
})
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
// Note: This should go in the banner's `action_slot`, but doing that messes with the size of the
|
||||
// progress bar.
|
||||
.child(action),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UsageBanner {
|
||||
fn sort_name() -> &'static str {
|
||||
"AgentUsageBanner"
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let trial_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro Trial - New User",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 10))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 135))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro Trial - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedProTrial, 150))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let free_examples = vec![
|
||||
single_example(
|
||||
"Free - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 25))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 45))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::Free, 50))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
let zed_pro_examples = vec![
|
||||
single_example(
|
||||
"Zed Pro - Normal Usage",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 250))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Approaching Limit",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 450))
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Zed Pro - Request Limit Reached",
|
||||
div()
|
||||
.size_full()
|
||||
.child(UsageBanner::new(Plan::ZedPro, 500))
|
||||
.into_any_element(),
|
||||
),
|
||||
];
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![
|
||||
Label::new("Trial Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(trial_examples).vertical().into_any_element(),
|
||||
Label::new("Free Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(free_examples).vertical().into_any_element(),
|
||||
Label::new("Pro Plan")
|
||||
.size(LabelSize::Large)
|
||||
.into_any_element(),
|
||||
example_group(zed_pro_examples)
|
||||
.vertical()
|
||||
.into_any_element(),
|
||||
])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use gpui::{Entity, Render};
|
||||
use ui::{ProgressBar, prelude::*};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct UserSpending {
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
free_tier_progress: Entity<ProgressBar>,
|
||||
over_tier_progress: Entity<ProgressBar>,
|
||||
}
|
||||
|
||||
impl UserSpending {
|
||||
pub fn new(
|
||||
free_tier_current: u32,
|
||||
free_tier_cap: u32,
|
||||
over_tier_current: u32,
|
||||
over_tier_cap: u32,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let free_tier_capped = free_tier_current == free_tier_cap;
|
||||
let free_tier_near_capped =
|
||||
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
|
||||
let over_tier_capped = over_tier_current == over_tier_cap;
|
||||
let over_tier_near_capped =
|
||||
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
|
||||
|
||||
let free_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"free_tier",
|
||||
free_tier_current as f32,
|
||||
free_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let over_tier_progress = cx.new(|cx| {
|
||||
ProgressBar::new(
|
||||
"over_tier",
|
||||
over_tier_current as f32,
|
||||
over_tier_cap as f32,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if free_tier_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if free_tier_near_capped {
|
||||
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
if over_tier_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().error);
|
||||
});
|
||||
} else if over_tier_near_capped {
|
||||
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||
progress_bar.fg_color(cx.theme().status().warning);
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
free_tier_current,
|
||||
free_tier_cap,
|
||||
over_tier_current,
|
||||
over_tier_cap,
|
||||
free_tier_progress,
|
||||
over_tier_progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for UserSpending {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let formatted_free_tier = format!(
|
||||
"${} / ${}",
|
||||
self.free_tier_current as f32 / 100.0,
|
||||
self.free_tier_cap as f32 / 100.0
|
||||
);
|
||||
let formatted_over_tier = format!(
|
||||
"${} / ${}",
|
||||
self.over_tier_current as f32 / 100.0,
|
||||
self.over_tier_cap as f32 / 100.0
|
||||
);
|
||||
|
||||
v_group()
|
||||
.elevation_2(cx)
|
||||
.py_1p5()
|
||||
.px_2p5()
|
||||
.w(px(360.))
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_free_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.free_tier_progress.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1p5()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Current Spending").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(formatted_over_tier)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(self.over_tier_progress.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UserSpending {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
|
||||
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
|
||||
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
|
||||
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
|
||||
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.p_4()
|
||||
.children(vec![example_group(vec![
|
||||
single_example(
|
||||
"New User",
|
||||
div().size_full().child(new_user.clone()).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Free Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(free_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Near Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_near_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Over Tier Capped",
|
||||
div()
|
||||
.size_full()
|
||||
.child(over_capped.clone())
|
||||
.into_any_element(),
|
||||
),
|
||||
])])
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use assistant_context_editor::{
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use client::{Client, Status, proto};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
|
||||
@@ -27,10 +27,13 @@ use language_model::{
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use prompt_store::{PromptBuilder, PromptId};
|
||||
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
@@ -59,7 +62,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
|
||||
panel.deploy_prompt_library(&OpenPromptLibrary::default(), window, cx)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -269,7 +272,10 @@ impl AssistantPanel {
|
||||
menu.context(focus_handle.clone())
|
||||
.action("New Chat", Box::new(NewChat))
|
||||
.action("History", Box::new(DeployHistory))
|
||||
.action("Prompt Library", Box::new(OpenPromptLibrary))
|
||||
.action(
|
||||
"Prompt Library",
|
||||
Box::new(OpenPromptLibrary::default()),
|
||||
)
|
||||
.action("Configure", Box::new(ShowConfiguration))
|
||||
.action(zoom_label, Box::new(ToggleZoom))
|
||||
}))
|
||||
@@ -1040,7 +1046,7 @@ impl AssistantPanel {
|
||||
|
||||
fn deploy_prompt_library(
|
||||
&mut self,
|
||||
_: &OpenPromptLibrary,
|
||||
action: &OpenPromptLibrary,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1054,6 +1060,7 @@ impl AssistantPanel {
|
||||
None,
|
||||
))
|
||||
}),
|
||||
action.prompt_to_focus.map(|uuid| PromptId::User { uuid }),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -1413,7 +1420,8 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
@@ -1425,6 +1433,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selection_ranges = selection_ranges
|
||||
.into_iter()
|
||||
.map(|range| range.to_point(&snapshot))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
panel.update(cx, |_, cx| {
|
||||
// Wait to create a new context until the workspace is no longer
|
||||
// being updated.
|
||||
@@ -1433,7 +1447,9 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
|
||||
.active_context_editor(cx)
|
||||
.or_else(|| panel.new_context(window, cx))
|
||||
{
|
||||
context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
|
||||
context.update(cx, |context, cx| {
|
||||
context.quote_ranges(selection_ranges, snapshot, window, cx)
|
||||
});
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2978,6 +2978,8 @@ impl CodegenAlternative {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -292,6 +292,8 @@ impl TerminalInlineAssistant {
|
||||
});
|
||||
|
||||
Ok(LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -2555,6 +2555,8 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let mut completion_request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
|
||||
@@ -8,8 +8,8 @@ use assistant_slash_commands::{
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
|
||||
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{MoveToEndOfLine, Newline, ShowCompletions},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
@@ -155,7 +155,8 @@ pub trait AssistantPanelDelegate {
|
||||
fn quote_selection(
|
||||
&self,
|
||||
workspace: &mut Workspace,
|
||||
creases: Vec<(String, String)>,
|
||||
selection_ranges: Vec<Range<Anchor>>,
|
||||
buffer: Entity<MultiBuffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
);
|
||||
@@ -1800,23 +1801,45 @@ impl ContextEditor {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(creases) = selections_creases(workspace, cx) else {
|
||||
let Some((selections, buffer)) = maybe!({
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let selections = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.into_iter()
|
||||
.filter_map(|s| {
|
||||
(!s.is_empty())
|
||||
.then(|| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
Some((selections, buffer))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if creases.is_empty() {
|
||||
if selections.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
assistant_panel_delegate.quote_selection(workspace, creases, window, cx);
|
||||
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
|
||||
}
|
||||
|
||||
pub fn quote_creases(
|
||||
pub fn quote_ranges(
|
||||
&mut self,
|
||||
creases: Vec<(String, String)>,
|
||||
ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let creases = selections_creases(ranges, snapshot, cx);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.insert("\n", window, cx);
|
||||
for (text, crease_title) in creases {
|
||||
|
||||
@@ -54,9 +54,9 @@ impl SlashCommand for DefaultSlashCommand {
|
||||
cx: &mut App,
|
||||
) -> Task<SlashCommandResult> {
|
||||
let store = PromptStore::global(cx);
|
||||
cx.background_spawn(async move {
|
||||
cx.spawn(async move |cx| {
|
||||
let store = store.await?;
|
||||
let prompts = store.default_prompt_metadata();
|
||||
let prompts = store.read_with(cx, |store, _cx| store.default_prompt_metadata())?;
|
||||
|
||||
let mut text = String::new();
|
||||
text.push('\n');
|
||||
|
||||
@@ -5,7 +5,7 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, LspAdapterDelegate};
|
||||
use prompt_store::PromptStore;
|
||||
use prompt_store::{PromptMetadata, PromptStore};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
@@ -43,8 +43,11 @@ impl SlashCommand for PromptSlashCommand {
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
let query = arguments.to_owned().join(" ");
|
||||
cx.background_spawn(async move {
|
||||
let prompts = store.await?.search(query).await;
|
||||
cx.spawn(async move |cx| {
|
||||
let prompts: Vec<PromptMetadata> = store
|
||||
.await?
|
||||
.read_with(cx, |store, cx| store.search(query, cx))?
|
||||
.await;
|
||||
Ok(prompts
|
||||
.into_iter()
|
||||
.filter_map(|prompt| {
|
||||
@@ -77,14 +80,18 @@ impl SlashCommand for PromptSlashCommand {
|
||||
|
||||
let store = PromptStore::global(cx);
|
||||
let title = SharedString::from(title.clone());
|
||||
let prompt = cx.background_spawn({
|
||||
let prompt = cx.spawn({
|
||||
let title = title.clone();
|
||||
async move {
|
||||
async move |cx| {
|
||||
let store = store.await?;
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
let body = store.load(prompt_id).await?;
|
||||
let body = store
|
||||
.read_with(cx, |store, cx| {
|
||||
let prompt_id = store
|
||||
.id_for_title(&title)
|
||||
.with_context(|| format!("no prompt found with title {:?}", title))?;
|
||||
anyhow::Ok(store.load(prompt_id, cx))
|
||||
})??
|
||||
.await?;
|
||||
anyhow::Ok(body)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,10 +3,12 @@ use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent,
|
||||
SlashCommandOutputSection, SlashCommandResult,
|
||||
};
|
||||
use editor::Editor;
|
||||
use editor::{Editor, MultiBufferSnapshot};
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Context, SharedString, Task, WeakEntity, Window};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use rope::Point;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use ui::IconName;
|
||||
@@ -69,7 +71,22 @@ impl SlashCommand for SelectionCommand {
|
||||
let mut events = vec![];
|
||||
|
||||
let Some(creases) = workspace
|
||||
.update(cx, selections_creases)
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selection_ranges = editor
|
||||
.selections
|
||||
.all_adjusted(cx)
|
||||
.iter()
|
||||
.map(|selection| selection.range())
|
||||
.collect::<Vec<_>>();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
Some(selections_creases(selection_ranges, snapshot, cx))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|e| {
|
||||
events.push(Err(e));
|
||||
None
|
||||
@@ -102,94 +119,82 @@ impl SlashCommand for SelectionCommand {
|
||||
}
|
||||
|
||||
pub fn selections_creases(
|
||||
workspace: &mut workspace::Workspace,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Option<Vec<(String, String)>> {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
||||
|
||||
let mut creases = vec![];
|
||||
editor.update(cx, |editor, cx| {
|
||||
let selections = editor.selections.all_adjusted(cx);
|
||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in selections {
|
||||
let range = editor::ToOffset::to_offset(&selection.start, &buffer)
|
||||
..editor::ToOffset::to_offset(&selection.end, &buffer);
|
||||
let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let start_language = buffer.language_at(range.start);
|
||||
let end_language = buffer.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = buffer
|
||||
.file_at(selection.start)
|
||||
.map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = buffer
|
||||
.symbols_containing(selection.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = buffer
|
||||
.symbols_containing(selection.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix = start_language
|
||||
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence = codeblock_fence_for_path(
|
||||
filename.as_deref(),
|
||||
Some(selection.start.row..=selection.end.row),
|
||||
);
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) =
|
||||
line_comment_prefix.zip(outline_text)
|
||||
{
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = selection.start.row + 1;
|
||||
let end_line = selection.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
creases.push((text, crease_title));
|
||||
selection_ranges: Vec<Range<Point>>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
cx: &App,
|
||||
) -> Vec<(String, String)> {
|
||||
let mut creases = Vec::new();
|
||||
for range in selection_ranges {
|
||||
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
if selected_text.is_empty() {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
Some(creases)
|
||||
let start_language = snapshot.language_at(range.start);
|
||||
let end_language = snapshot.language_at(range.end);
|
||||
let language_name = if start_language == end_language {
|
||||
start_language.map(|language| language.code_fence_block_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref().unwrap_or("");
|
||||
let filename = snapshot.file_at(range.start).map(|file| file.full_path(cx));
|
||||
let text = if language_name == "markdown" {
|
||||
selected_text
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
let start_symbols = snapshot
|
||||
.symbols_containing(range.start, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
let end_symbols = snapshot
|
||||
.symbols_containing(range.end, None)
|
||||
.map(|(_, symbols)| symbols);
|
||||
|
||||
let outline_text =
|
||||
if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
|
||||
Some(
|
||||
start_symbols
|
||||
.into_iter()
|
||||
.zip(end_symbols)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.map(|(a, _)| a.text)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" > "),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let line_comment_prefix = start_language
|
||||
.and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
|
||||
|
||||
let fence = codeblock_fence_for_path(
|
||||
filename.as_deref(),
|
||||
Some(range.start.row..=range.end.row),
|
||||
);
|
||||
|
||||
if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
|
||||
{
|
||||
let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
|
||||
format!("{fence}{breadcrumb}{selected_text}\n```")
|
||||
} else {
|
||||
format!("{fence}{selected_text}\n```")
|
||||
}
|
||||
};
|
||||
let crease_title = if let Some(path) = filename {
|
||||
let start_line = range.start.row + 1;
|
||||
let end_line = range.end.row + 1;
|
||||
if start_line == end_line {
|
||||
format!("{}, Line {}", path.display(), start_line)
|
||||
} else {
|
||||
format!("{}, Lines {} to {}", path.display(), start_line, end_line)
|
||||
}
|
||||
} else {
|
||||
"Quoted selection".to_string()
|
||||
};
|
||||
creases.push((text, crease_title));
|
||||
}
|
||||
creases
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::AnyElement;
|
||||
use gpui::Context;
|
||||
use gpui::IntoElement;
|
||||
use gpui::Window;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
@@ -24,16 +28,87 @@ pub fn init(cx: &mut App) {
|
||||
ToolRegistry::default_global(cx);
|
||||
}
|
||||
|
||||
/// The result of running a tool
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ToolUseStatus {
|
||||
NeedsConfirmation,
|
||||
Pending,
|
||||
Running,
|
||||
Finished(SharedString),
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl ToolUseStatus {
|
||||
pub fn text(&self) -> SharedString {
|
||||
match self {
|
||||
ToolUseStatus::NeedsConfirmation => "".into(),
|
||||
ToolUseStatus::Pending => "".into(),
|
||||
ToolUseStatus::Running => "".into(),
|
||||
ToolUseStatus::Finished(out) => out.clone(),
|
||||
ToolUseStatus::Error(out) => out.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of running a tool, containing both the asynchronous output
|
||||
/// and an optional card view that can be rendered immediately.
|
||||
pub struct ToolResult {
|
||||
/// The asynchronous task that will eventually resolve to the tool's output
|
||||
pub output: Task<Result<String>>,
|
||||
/// An optional view to present the output of the tool.
|
||||
pub card: Option<AnyToolCard>,
|
||||
}
|
||||
|
||||
pub trait ToolCard: 'static + Sized {
|
||||
fn render(
|
||||
&mut self,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
render: fn(
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement,
|
||||
}
|
||||
|
||||
impl<T: ToolCard> From<Entity<T>> for AnyToolCard {
|
||||
fn from(entity: Entity<T>) -> Self {
|
||||
fn downcast_render<T: ToolCard>(
|
||||
entity: gpui::AnyEntity,
|
||||
status: &ToolUseStatus,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let entity = entity.downcast::<T>().unwrap();
|
||||
entity.update(cx, |entity, cx| {
|
||||
entity.render(status, window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
Self {
|
||||
entity: entity.into(),
|
||||
render: downcast_render::<T>,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyToolCard {
|
||||
pub fn render(&self, status: &ToolUseStatus, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
(self.render)(self.entity.clone(), status, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Task<Result<String>>> for ToolResult {
|
||||
/// Convert from a task to a ToolResult
|
||||
/// Convert from a task to a ToolResult with no card
|
||||
fn from(output: Task<Result<String>>) -> Self {
|
||||
Self { output }
|
||||
Self { output, card: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -32,7 +33,9 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -22,14 +22,17 @@ mod schema;
|
||||
mod symbol_info_tool;
|
||||
mod terminal_tool;
|
||||
mod thinking_tool;
|
||||
mod web_search_tool;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::App;
|
||||
use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
use crate::batch_tool::BatchTool;
|
||||
use crate::code_action_tool::CodeActionTool;
|
||||
@@ -56,28 +59,39 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(BatchTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(CodeSymbolsTool);
|
||||
registry.register_tool(ContentsTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
registry.register_tool(PathSearchTool);
|
||||
registry.register_tool(ReadFileTool);
|
||||
registry.register_tool(RegexSearchTool);
|
||||
registry.register_tool(RenameTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(TerminalTool);
|
||||
registry.register_tool(ThinkingTool);
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
cx.observe_flag::<feature_flags::ZedProWebSearchTool, _>({
|
||||
move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
ToolRegistry::global(cx).register_tool(WebSearchTool);
|
||||
} else {
|
||||
ToolRegistry::global(cx).unregister_tool(WebSearchTool);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
213
crates/assistant_tools/src/web_search_tool.rs
Normal file
213
crates/assistant_tools/src/web_search_tool.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, AppContext, Context, Entity, IntoElement, Task, Window,
|
||||
pulsating_between,
|
||||
};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
/// The search term or question to query on the web.
|
||||
query: String,
|
||||
}
|
||||
|
||||
pub struct WebSearchTool;
|
||||
|
||||
impl Tool for WebSearchTool {
|
||||
fn name(&self) -> String {
|
||||
"web_search".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Globe
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<WebSearchToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, _input: &serde_json::Value) -> String {
|
||||
"Web Search".to_string()
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input = match serde_json::from_value::<WebSearchToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
|
||||
};
|
||||
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
|
||||
return Task::ready(Err(anyhow!("Web search is not available."))).into();
|
||||
};
|
||||
|
||||
let search_task = provider.search(input.query, cx).map_err(Arc::new).shared();
|
||||
let output = cx.background_spawn({
|
||||
let search_task = search_task.clone();
|
||||
async move {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err))?;
|
||||
serde_json::to_string(&response).context("Failed to serialize search results")
|
||||
}
|
||||
});
|
||||
|
||||
ToolResult {
|
||||
output,
|
||||
card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebSearchToolCard {
|
||||
response: Option<Result<WebSearchResponse>>,
|
||||
_task: Task<()>,
|
||||
}
|
||||
|
||||
impl WebSearchToolCard {
|
||||
fn new(
|
||||
search_task: impl 'static + Future<Output = Result<WebSearchResponse, Arc<anyhow::Error>>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn(async move |this, cx| {
|
||||
let response = search_task.await.map_err(|err| anyhow!(err));
|
||||
this.update(cx, |this, cx| {
|
||||
this.response = Some(response);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Self {
|
||||
response: None,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCard for WebSearchToolCard {
|
||||
fn render(
|
||||
&mut self,
|
||||
_status: &ToolUseStatus,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let header = h_flex()
|
||||
.id("tool-label-container")
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(IconName::Globe)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(match self.response.as_ref() {
|
||||
Some(Ok(response)) => {
|
||||
let text: SharedString = if response.citations.len() == 1 {
|
||||
"1 result".into()
|
||||
} else {
|
||||
format!("{} results", response.citations.len()).into()
|
||||
};
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(Label::new("Searched the Web").size(LabelSize::Small))
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text),
|
||||
)
|
||||
.child(Label::new(text).size(LabelSize::Small))
|
||||
.into_any_element()
|
||||
}
|
||||
Some(Err(error)) => div()
|
||||
.id("web-search-error")
|
||||
.child(Label::new("Web Search failed").size(LabelSize::Small))
|
||||
.tooltip(Tooltip::text(error.to_string()))
|
||||
.into_any_element(),
|
||||
|
||||
None => Label::new("Searching the Web…")
|
||||
.size(LabelSize::Small)
|
||||
.with_animation(
|
||||
"web-search-label",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.6, 1.)),
|
||||
|label, delta| label.alpha(delta),
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
.into_any();
|
||||
|
||||
let content =
|
||||
self.response.as_ref().and_then(|response| match response {
|
||||
Ok(response) => {
|
||||
Some(
|
||||
v_flex()
|
||||
.ml_1p5()
|
||||
.pl_1p5()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.gap_1()
|
||||
.children(response.citations.iter().enumerate().map(
|
||||
|(index, citation)| {
|
||||
let title = citation.title.clone();
|
||||
let url = citation.url.clone();
|
||||
|
||||
Button::new(("citation", index), title)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.truncate(true)
|
||||
.tooltip({
|
||||
let url = url.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Citation Link",
|
||||
None,
|
||||
url.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click({
|
||||
let url = url.clone();
|
||||
move |_, _, cx| cx.open_url(&url)
|
||||
})
|
||||
},
|
||||
))
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
Err(_) => None,
|
||||
});
|
||||
|
||||
v_flex().my_2().gap_1().child(header).children(content)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
alter table subscription_usages
|
||||
add column plan text not null;
|
||||
|
||||
create index ix_subscription_usages_on_plan on subscription_usages (plan);
|
||||
@@ -330,8 +330,10 @@ async fn create_billing_subscription(
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
let default_model =
|
||||
llm_db.model(rpc::LanguageModelProvider::Anthropic, "claude-3-7-sonnet")?;
|
||||
let default_model = llm_db.model(
|
||||
zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
"claude-3-7-sonnet",
|
||||
)?;
|
||||
let stripe_model = stripe_billing.register_model(default_model).await?;
|
||||
stripe_billing
|
||||
.checkout(customer_id, &user.github_login, &stripe_model, &success_url)
|
||||
@@ -1018,8 +1020,20 @@ async fn get_current_usage(
|
||||
return Ok(Json(empty_usage));
|
||||
};
|
||||
|
||||
let model_requests_limit = Some(500);
|
||||
let edit_prediction_limit = Some(2000);
|
||||
let plan = match usage.plan {
|
||||
SubscriptionKind::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => zed_llm_client::Plan::Free,
|
||||
};
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
|
||||
zed_llm_client::UsageLimit::Unlimited => None,
|
||||
};
|
||||
let edit_prediction_limit = match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
|
||||
zed_llm_client::UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
Ok(Json(GetCurrentUsageResponse {
|
||||
model_requests: UsageCounts {
|
||||
|
||||
@@ -8,9 +8,9 @@ mod tests;
|
||||
|
||||
use collections::HashMap;
|
||||
pub use ids::*;
|
||||
use rpc::LanguageModelProvider;
|
||||
pub use seed::*;
|
||||
pub use tables::*;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestLlmDb;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::db::UserId;
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
@@ -10,6 +11,7 @@ pub struct Model {
|
||||
pub user_id: UserId,
|
||||
pub period_start_at: PrimitiveDateTime,
|
||||
pub period_end_at: PrimitiveDateTime,
|
||||
pub plan: SubscriptionKind,
|
||||
pub model_requests: i32,
|
||||
pub edit_predictions: i32,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
use rpc::LanguageModelProvider;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::Plan;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -29,7 +30,7 @@ pub struct LlmTokenClaims {
|
||||
pub has_llm_subscription: bool,
|
||||
pub max_monthly_spend_in_cents: u32,
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
pub plan: rpc::proto::Plan,
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub subscription_period: Option<(NaiveDateTime, NaiveDateTime)>,
|
||||
}
|
||||
@@ -81,7 +82,11 @@ impl LlmTokenClaims {
|
||||
custom_llm_monthly_allowance_in_cents: user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| allowance as u32),
|
||||
plan,
|
||||
plan: match plan {
|
||||
rpc::proto::Plan::Free => Plan::Free,
|
||||
rpc::proto::Plan::ZedPro => Plan::ZedPro,
|
||||
rpc::proto::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
},
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
|
||||
@@ -3707,7 +3707,9 @@ async fn count_language_model_tokens(
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
|
||||
proto::Plan::ZedPro => Box::new(ZedProCountLanguageModelTokensRateLimit),
|
||||
proto::Plan::Free => Box::new(FreeCountLanguageModelTokensRateLimit),
|
||||
proto::Plan::Free | proto::Plan::ZedProTrial => {
|
||||
Box::new(FreeCountLanguageModelTokensRateLimit)
|
||||
}
|
||||
};
|
||||
|
||||
session
|
||||
@@ -3827,7 +3829,7 @@ async fn compute_embeddings(
|
||||
|
||||
let rate_limit: Box<dyn RateLimit> = match session.current_plan(&session.db().await).await? {
|
||||
proto::Plan::ZedPro => Box::new(ZedProComputeEmbeddingsRateLimit),
|
||||
proto::Plan::Free => Box::new(FreeComputeEmbeddingsRateLimit),
|
||||
proto::Plan::Free | proto::Plan::ZedProTrial => Box::new(FreeComputeEmbeddingsRateLimit),
|
||||
};
|
||||
|
||||
session
|
||||
|
||||
@@ -41,6 +41,10 @@ pub enum Model {
|
||||
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")]
|
||||
@@ -63,6 +67,8 @@ impl Model {
|
||||
| Self::Gpt4
|
||||
| Self::Gpt4_1
|
||||
| Self::Gpt3_5Turbo
|
||||
| Self::O3
|
||||
| Self::O4Mini
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking => true,
|
||||
@@ -78,6 +84,8 @@ impl Model {
|
||||
"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),
|
||||
@@ -95,6 +103,8 @@ impl Model {
|
||||
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",
|
||||
@@ -111,6 +121,8 @@ impl Model {
|
||||
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",
|
||||
@@ -123,10 +135,12 @@ impl Model {
|
||||
match self {
|
||||
Self::Gpt4o => 64_000,
|
||||
Self::Gpt4 => 32_768,
|
||||
Self::Gpt4_1 => 1_047_576,
|
||||
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,
|
||||
|
||||
@@ -46,7 +46,8 @@ use workspace::{
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
|
||||
struct IncludeWarnings(bool);
|
||||
#[derive(Default)]
|
||||
pub(crate) struct IncludeWarnings(bool);
|
||||
impl Global for IncludeWarnings {}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -379,7 +380,6 @@ impl ProjectDiagnosticsEditor {
|
||||
Point::zero()..buffer_snapshot.max_point(),
|
||||
false,
|
||||
)
|
||||
.filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
|
||||
.collect::<Vec<_>>();
|
||||
let unchanged = this.update(cx, |this, _| {
|
||||
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use super::*;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
DisplayPoint,
|
||||
DisplayPoint, InlayId,
|
||||
actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
|
||||
display_map::DisplayRow,
|
||||
display_map::{DisplayRow, Inlay},
|
||||
test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
|
||||
};
|
||||
use gpui::{TestAppContext, VisualTestContext};
|
||||
@@ -620,7 +620,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 20)]
|
||||
async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
@@ -779,6 +779,162 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
}
|
||||
}
|
||||
|
||||
// similar to above, but with inlays. Used to find panics when mixing diagnostics and inlays.
|
||||
#[gpui::test]
|
||||
async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(path!("/test"), json!({})).await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
|
||||
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
|
||||
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let mutated_diagnostics = window.build_entity(cx, |window, cx| {
|
||||
ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
|
||||
});
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_center(Box::new(mutated_diagnostics.clone()), window, cx);
|
||||
});
|
||||
mutated_diagnostics.update_in(cx, |diagnostics, window, _cx| {
|
||||
assert!(diagnostics.focus_handle.is_focused(window));
|
||||
});
|
||||
|
||||
let mut next_id = 0;
|
||||
let mut next_filename = 0;
|
||||
let mut language_server_ids = vec![LanguageServerId(0)];
|
||||
let mut updated_language_servers = HashSet::default();
|
||||
let mut current_diagnostics: HashMap<(PathBuf, LanguageServerId), Vec<lsp::Diagnostic>> =
|
||||
Default::default();
|
||||
let mut next_inlay_id = 0;
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
// language server completes its diagnostic check
|
||||
0..=20 if !updated_language_servers.is_empty() => {
|
||||
let server_id = *updated_language_servers.iter().choose(&mut rng).unwrap();
|
||||
log::info!("finishing diagnostic check for language server {server_id}");
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.disk_based_diagnostics_finished(server_id, cx)
|
||||
});
|
||||
|
||||
if rng.gen_bool(0.5) {
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
|
||||
21..=50 => mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
if snapshot.buffer_snapshot.len() > 0 {
|
||||
let position = rng.gen_range(0..snapshot.buffer_snapshot.len());
|
||||
let position = snapshot.buffer_snapshot.clip_offset(position, Bias::Left);
|
||||
log::info!(
|
||||
"adding inlay at {position}/{}: {:?}",
|
||||
snapshot.buffer_snapshot.len(),
|
||||
snapshot.buffer_snapshot.text(),
|
||||
);
|
||||
|
||||
editor.splice_inlays(
|
||||
&[],
|
||||
vec![Inlay {
|
||||
id: InlayId::InlineCompletion(post_inc(&mut next_inlay_id)),
|
||||
position: snapshot.buffer_snapshot.anchor_before(position),
|
||||
text: Rope::from(format!("Test inlay {next_inlay_id}")),
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
// language server updates diagnostics
|
||||
_ => {
|
||||
let (path, server_id, diagnostics) =
|
||||
match current_diagnostics.iter_mut().choose(&mut rng) {
|
||||
// update existing set of diagnostics
|
||||
Some(((path, server_id), diagnostics)) if rng.gen_bool(0.5) => {
|
||||
(path.clone(), *server_id, diagnostics)
|
||||
}
|
||||
|
||||
// insert a set of diagnostics for a new path
|
||||
_ => {
|
||||
let path: PathBuf =
|
||||
format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
|
||||
let len = rng.gen_range(128..256);
|
||||
let content =
|
||||
RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
fs.insert_file(&path, content.into_bytes()).await;
|
||||
|
||||
let server_id = match language_server_ids.iter().choose(&mut rng) {
|
||||
Some(server_id) if rng.gen_bool(0.5) => *server_id,
|
||||
_ => {
|
||||
let id = LanguageServerId(language_server_ids.len());
|
||||
language_server_ids.push(id);
|
||||
id
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
path.clone(),
|
||||
server_id,
|
||||
current_diagnostics.entry((path, server_id)).or_default(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
updated_language_servers.insert(server_id);
|
||||
|
||||
lsp_store.update(cx, |lsp_store, cx| {
|
||||
log::info!("updating diagnostics. language server {server_id} path {path:?}");
|
||||
randomly_update_diagnostics_for_path(
|
||||
&fs,
|
||||
&path,
|
||||
diagnostics,
|
||||
&mut next_id,
|
||||
&mut rng,
|
||||
);
|
||||
lsp_store
|
||||
.update_diagnostics(
|
||||
server_id,
|
||||
lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(&path).unwrap_or_else(|_| {
|
||||
lsp::Url::parse("file:///test/fallback.rs").unwrap()
|
||||
}),
|
||||
diagnostics: diagnostics.clone(),
|
||||
version: None,
|
||||
},
|
||||
&[],
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
|
||||
|
||||
cx.run_until_parked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("updating mutated diagnostics view");
|
||||
mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
|
||||
diagnostics.update_stale_excerpts(window, cx)
|
||||
});
|
||||
|
||||
cx.executor()
|
||||
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -9,7 +9,7 @@ use language::Diagnostic;
|
||||
use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
|
||||
use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
|
||||
|
||||
use crate::{Deploy, ProjectDiagnosticsEditor};
|
||||
use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
@@ -94,6 +94,11 @@ impl Render for DiagnosticIndicator {
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade() {
|
||||
if this.summary.error_count == 0 && this.summary.warning_count > 0 {
|
||||
cx.update_default_global(
|
||||
|show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
|
||||
);
|
||||
}
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(
|
||||
workspace,
|
||||
|
||||
@@ -632,6 +632,7 @@ impl CompletionsMenu {
|
||||
MarkdownElement::new(markdown.clone(), hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
)
|
||||
@@ -776,11 +777,34 @@ pub struct AvailableCodeAction {
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CodeActionContents {
|
||||
pub tasks: Option<Rc<ResolvedTasks>>,
|
||||
pub actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
tasks: Option<Rc<ResolvedTasks>>,
|
||||
pub(crate) actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
}
|
||||
|
||||
impl CodeActionContents {
|
||||
pub fn new(
|
||||
mut tasks: Option<ResolvedTasks>,
|
||||
actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
cx: &App,
|
||||
) -> Self {
|
||||
if !cx.has_flag::<Debugger>() {
|
||||
if let Some(tasks) = &mut tasks {
|
||||
tasks
|
||||
.templates
|
||||
.retain(|(_, task)| !matches!(task.task_type(), task::TaskType::Debug(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
tasks: tasks.map(Rc::new),
|
||||
actions,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tasks(&self) -> Option<&ResolvedTasks> {
|
||||
self.tasks.as_deref()
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
match (&self.tasks, &self.actions) {
|
||||
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
|
||||
@@ -988,17 +1012,6 @@ impl CodeActionsMenu {
|
||||
.iter()
|
||||
.skip(range.start)
|
||||
.take(range.end - range.start)
|
||||
.filter(|action| {
|
||||
if action
|
||||
.as_task()
|
||||
.map(|task| matches!(task.task_type(), task::TaskType::Debug(_)))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
cx.has_flag::<Debugger>()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(ix, action)| {
|
||||
let item_ix = range.start + ix;
|
||||
|
||||
@@ -49,8 +49,8 @@ use language::{
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot,
|
||||
RowInfo, ToOffset, ToPoint,
|
||||
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@@ -574,6 +574,21 @@ impl DisplayMap {
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) {
|
||||
let to_remove = self
|
||||
.inlay_map
|
||||
.current_inlays()
|
||||
.filter_map(|inlay| {
|
||||
if excerpts_removed.contains(&inlay.position.excerpt_id) {
|
||||
Some(inlay.id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.inlay_map.splice(&to_remove, Vec::new());
|
||||
}
|
||||
|
||||
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
|
||||
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
|
||||
let language = buffer
|
||||
|
||||
@@ -36,7 +36,7 @@ enum Transform {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Inlay {
|
||||
pub(crate) id: InlayId,
|
||||
pub id: InlayId,
|
||||
pub position: Anchor,
|
||||
pub text: text::Rope,
|
||||
}
|
||||
@@ -482,6 +482,9 @@ impl InlayMap {
|
||||
};
|
||||
|
||||
for inlay in &self.inlays[start_ix..] {
|
||||
if !inlay.position.is_valid(&buffer_snapshot) {
|
||||
continue;
|
||||
}
|
||||
let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
|
||||
if buffer_offset > buffer_edit.new.end {
|
||||
break;
|
||||
@@ -494,9 +497,7 @@ impl InlayMap {
|
||||
buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
|
||||
);
|
||||
|
||||
if inlay.position.is_valid(&buffer_snapshot) {
|
||||
new_transforms.push(Transform::Inlay(inlay.clone()), &());
|
||||
}
|
||||
new_transforms.push(Transform::Inlay(inlay.clone()), &());
|
||||
}
|
||||
|
||||
// Apply the rest of the edit.
|
||||
|
||||
@@ -4170,10 +4170,13 @@ impl Editor {
|
||||
if let Some(InlaySplice {
|
||||
to_remove,
|
||||
to_insert,
|
||||
}) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
|
||||
}) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed)
|
||||
{
|
||||
self.splice_inlays(&to_remove, to_insert, cx);
|
||||
}
|
||||
self.display_map.update(cx, |display_map, _| {
|
||||
display_map.remove_inlays_for_excerpts(&excerpts_removed)
|
||||
});
|
||||
return;
|
||||
}
|
||||
InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
|
||||
@@ -4741,19 +4744,18 @@ impl Editor {
|
||||
let suffix = &old_text[lookbehind.min(old_text.len())..];
|
||||
|
||||
let selections = self.selections.all::<usize>(cx);
|
||||
let mut edits = Vec::new();
|
||||
let mut ranges = Vec::new();
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
|
||||
for selection in &selections {
|
||||
let edit = if selection.id == newest_anchor.id {
|
||||
(replace_range_multibuffer.clone(), new_text.as_str())
|
||||
let range = if selection.id == newest_anchor.id {
|
||||
replace_range_multibuffer.clone()
|
||||
} else {
|
||||
let mut range = selection.range();
|
||||
let mut text = new_text.as_str();
|
||||
|
||||
// if prefix is present, don't duplicate it
|
||||
if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) {
|
||||
text = &new_text[lookbehind.min(new_text.len())..];
|
||||
range.start = range.start.saturating_sub(lookbehind);
|
||||
|
||||
// if suffix is also present, mimic the newest cursor and replace it
|
||||
if selection.id != newest_anchor.id
|
||||
@@ -4762,10 +4764,10 @@ impl Editor {
|
||||
range.end += lookahead;
|
||||
}
|
||||
}
|
||||
(range, text)
|
||||
range
|
||||
};
|
||||
|
||||
edits.push(edit);
|
||||
ranges.push(range);
|
||||
|
||||
if !self.linked_edit_ranges.is_empty() {
|
||||
let start_anchor = snapshot.anchor_before(selection.head());
|
||||
@@ -4791,19 +4793,14 @@ impl Editor {
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = new_text.to_string();
|
||||
let ranges = edits
|
||||
.iter()
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
this.insert_snippet(&ranges, snippet, window, cx).log_err();
|
||||
} else {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let auto_indent = if completion.insert_text_mode == Some(InsertTextMode::AS_IS)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
this.autoindent_mode.clone()
|
||||
let auto_indent = match completion.insert_text_mode {
|
||||
Some(InsertTextMode::AS_IS) => None,
|
||||
_ => this.autoindent_mode.clone(),
|
||||
};
|
||||
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
||||
buffer.edit(edits, auto_indent, cx);
|
||||
});
|
||||
}
|
||||
@@ -4917,26 +4914,24 @@ impl Editor {
|
||||
Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, _| {
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let task_context = match task_context {
|
||||
Some(task_context) => task_context.await,
|
||||
None => None,
|
||||
};
|
||||
let resolved_tasks = tasks.zip(task_context).map(|(tasks, task_context)| {
|
||||
Rc::new(ResolvedTasks {
|
||||
templates: tasks.resolve(&task_context).collect(),
|
||||
position: snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(multibuffer_point.row, tasks.column)),
|
||||
})
|
||||
});
|
||||
Some((
|
||||
buffer,
|
||||
CodeActionContents {
|
||||
actions: code_actions,
|
||||
tasks: resolved_tasks,
|
||||
},
|
||||
))
|
||||
let resolved_tasks =
|
||||
tasks
|
||||
.zip(task_context)
|
||||
.map(|(tasks, task_context)| ResolvedTasks {
|
||||
templates: tasks.resolve(&task_context).collect(),
|
||||
position: snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_before(Point::new(multibuffer_point.row, tasks.column)),
|
||||
});
|
||||
let code_action_contents = cx
|
||||
.update(|_, cx| CodeActionContents::new(resolved_tasks, code_actions, cx))
|
||||
.ok()?;
|
||||
Some((buffer, code_action_contents))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4980,7 +4975,7 @@ impl Editor {
|
||||
Some(cx.spawn_in(window, async move |editor, cx| {
|
||||
if let Some((buffer, code_action_contents)) = code_actions_task.await {
|
||||
let spawn_straight_away =
|
||||
code_action_contents.tasks.as_ref().map_or(false, |tasks| {
|
||||
code_action_contents.tasks().map_or(false, |tasks| {
|
||||
tasks
|
||||
.templates
|
||||
.iter()
|
||||
@@ -10125,11 +10120,19 @@ impl Editor {
|
||||
..Point::new(row.0, buffer.line_len(row)),
|
||||
);
|
||||
for row in start.row + 1..=end.row {
|
||||
let mut line_len = buffer.line_len(MultiBufferRow(row));
|
||||
if row == end.row {
|
||||
line_len = end.column;
|
||||
}
|
||||
if line_len == 0 {
|
||||
trimmed_selections
|
||||
.push(Point::new(row, 0)..Point::new(row, line_len));
|
||||
continue;
|
||||
}
|
||||
let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
if row_indent_size.len >= first_indent.len {
|
||||
trimmed_selections.push(
|
||||
Point::new(row, first_indent.len)
|
||||
..Point::new(row, buffer.line_len(MultiBufferRow(row))),
|
||||
Point::new(row, first_indent.len)..Point::new(row, line_len),
|
||||
);
|
||||
} else {
|
||||
trimmed_selections.clear();
|
||||
|
||||
@@ -5121,6 +5121,36 @@ if is_entire_line {
|
||||
),
|
||||
"When selecting past the indent, nothing is trimmed"
|
||||
);
|
||||
|
||||
cx.set_state(
|
||||
r#" «for selection in selections.iter() {
|
||||
let mut start = selection.start;
|
||||
|
||||
let mut end = selection.end;
|
||||
let is_entire_line = selection.is_empty();
|
||||
if is_entire_line {
|
||||
start = Point::new(start.row, 0);
|
||||
ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
|
||||
}
|
||||
"#,
|
||||
);
|
||||
cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
|
||||
assert_eq!(
|
||||
cx.read_from_clipboard()
|
||||
.and_then(|item| item.text().as_deref().map(str::to_string)),
|
||||
Some(
|
||||
"for selection in selections.iter() {
|
||||
let mut start = selection.start;
|
||||
|
||||
let mut end = selection.end;
|
||||
let is_entire_line = selection.is_empty();
|
||||
if is_entire_line {
|
||||
start = Point::new(start.row, 0);
|
||||
"
|
||||
.to_string()
|
||||
),
|
||||
"Copying with stripping should ignore empty lines"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -9924,7 +9954,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContext) {
|
||||
async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
@@ -9938,6 +9968,8 @@ async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContex
|
||||
)
|
||||
.await;
|
||||
|
||||
// scenario: surrounding text matches completion text
|
||||
let completion_text = "to_offset";
|
||||
let initial_state = indoc! {"
|
||||
1. buf.to_offˇsuffix
|
||||
2. buf.to_offˇsuf
|
||||
@@ -9968,7 +10000,6 @@ async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContex
|
||||
|
||||
buf.<to_off|suffix> // newest cursor
|
||||
"};
|
||||
let completion_text = "to_offset";
|
||||
let expected = indoc! {"
|
||||
1. buf.to_offsetˇ
|
||||
2. buf.to_offsetˇsuf
|
||||
@@ -9984,24 +10015,122 @@ async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContex
|
||||
|
||||
buf.to_offsetˇ // newest cursor
|
||||
"};
|
||||
|
||||
cx.set_state(initial_state);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
|
||||
let counter = Arc::new(AtomicUsize::new(0));
|
||||
handle_completion_request_with_insert_and_replace(
|
||||
&mut cx,
|
||||
completion_marked_buffer,
|
||||
vec![completion_text],
|
||||
counter.clone(),
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(expected);
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// scenario: surrounding text matches surroundings of newest cursor, inserting at the end
|
||||
let completion_text = "foo_and_bar";
|
||||
let initial_state = indoc! {"
|
||||
1. ooanbˇ
|
||||
2. zooanbˇ
|
||||
3. ooanbˇz
|
||||
4. zooanbˇz
|
||||
5. ooanˇ
|
||||
6. oanbˇ
|
||||
|
||||
ooanbˇ
|
||||
"};
|
||||
let completion_marked_buffer = indoc! {"
|
||||
1. ooanb
|
||||
2. zooanb
|
||||
3. ooanbz
|
||||
4. zooanbz
|
||||
5. ooan
|
||||
6. oanb
|
||||
|
||||
<ooanb|>
|
||||
"};
|
||||
let expected = indoc! {"
|
||||
1. foo_and_barˇ
|
||||
2. zfoo_and_barˇ
|
||||
3. foo_and_barˇz
|
||||
4. zfoo_and_barˇz
|
||||
5. ooanfoo_and_barˇ
|
||||
6. oanbfoo_and_barˇ
|
||||
|
||||
foo_and_barˇ
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
handle_completion_request_with_insert_and_replace(
|
||||
&mut cx,
|
||||
completion_marked_buffer,
|
||||
vec![completion_text],
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
|
||||
.unwrap()
|
||||
});
|
||||
cx.assert_editor_state(expected);
|
||||
handle_resolve_completion_request(&mut cx, None).await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
|
||||
// scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
|
||||
// (expects the same as if it was inserted at the end)
|
||||
let completion_text = "foo_and_bar";
|
||||
let initial_state = indoc! {"
|
||||
1. ooˇanb
|
||||
2. zooˇanb
|
||||
3. ooˇanbz
|
||||
4. zooˇanbz
|
||||
|
||||
ooˇanb
|
||||
"};
|
||||
let completion_marked_buffer = indoc! {"
|
||||
1. ooanb
|
||||
2. zooanb
|
||||
3. ooanbz
|
||||
4. zooanbz
|
||||
|
||||
<oo|anb>
|
||||
"};
|
||||
let expected = indoc! {"
|
||||
1. foo_and_barˇ
|
||||
2. zfoo_and_barˇ
|
||||
3. foo_and_barˇz
|
||||
4. zfoo_and_barˇz
|
||||
|
||||
foo_and_barˇ
|
||||
"};
|
||||
cx.set_state(initial_state);
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
handle_completion_request_with_insert_and_replace(
|
||||
&mut cx,
|
||||
completion_marked_buffer,
|
||||
vec![completion_text],
|
||||
Arc::new(AtomicUsize::new(0)),
|
||||
)
|
||||
.await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
|
||||
|
||||
@@ -2093,8 +2093,7 @@ impl EditorElement {
|
||||
})) = editor.context_menu.borrow().as_ref()
|
||||
{
|
||||
actions
|
||||
.tasks
|
||||
.as_ref()
|
||||
.tasks()
|
||||
.map(|tasks| tasks.position.to_display_point(snapshot).row())
|
||||
.or(*deployed_from_indicator)
|
||||
} else {
|
||||
|
||||
@@ -841,6 +841,7 @@ impl InfoPopover {
|
||||
MarkdownElement::new(markdown, hover_markdown_style(window, cx))
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
),
|
||||
@@ -969,6 +970,7 @@ impl DiagnosticPopover {
|
||||
})
|
||||
.code_block_renderer(markdown::CodeBlockRenderer::Default {
|
||||
copy_button: false,
|
||||
border: false,
|
||||
})
|
||||
.on_url_click(open_markdown_url),
|
||||
)
|
||||
|
||||
@@ -555,12 +555,12 @@ impl InlayHintCache {
|
||||
/// Completely forget of certain excerpts that were removed from the multibuffer.
|
||||
pub(super) fn remove_excerpts(
|
||||
&mut self,
|
||||
excerpts_removed: Vec<ExcerptId>,
|
||||
excerpts_removed: &[ExcerptId],
|
||||
) -> Option<InlaySplice> {
|
||||
let mut to_remove = Vec::new();
|
||||
for excerpt_to_remove in excerpts_removed {
|
||||
self.update_tasks.remove(&excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
|
||||
self.update_tasks.remove(excerpt_to_remove);
|
||||
if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) {
|
||||
let cached_hints = cached_hints.read();
|
||||
to_remove.extend(cached_hints.ordered_hints.iter().copied());
|
||||
}
|
||||
|
||||
@@ -271,12 +271,12 @@ fn main() {
|
||||
match judge_result {
|
||||
Ok(judge_output) => {
|
||||
const SCORES: [&str; 6] = ["💀", "😭", "😔", "😐", "🙂", "🤩"];
|
||||
let score: u32 = judge_output.score;
|
||||
let score_index = (score.min(5)) as usize;
|
||||
|
||||
println!(
|
||||
"{} {}{}",
|
||||
SCORES[judge_output.score.min(5) as usize],
|
||||
example.log_prefix,
|
||||
judge_output.score,
|
||||
SCORES[score_index], example.log_prefix, judge_output.score,
|
||||
);
|
||||
judge_scores.push(judge_output.score);
|
||||
}
|
||||
@@ -304,7 +304,6 @@ fn main() {
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
// Flush telemetry events before exiting
|
||||
app_state.client.telemetry().flush_events();
|
||||
|
||||
cx.update(|cx| cx.quit())
|
||||
@@ -330,7 +329,6 @@ async fn run_example(
|
||||
for round in 0..judge_repetitions {
|
||||
let judge_result = example.judge(model.clone(), diff.clone(), round, cx).await;
|
||||
|
||||
// Log telemetry for this judge result
|
||||
if let Ok(judge_output) = &judge_result {
|
||||
let cohort_id = example
|
||||
.output_file_path
|
||||
@@ -339,6 +337,9 @@ async fn run_example(
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or(chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string());
|
||||
|
||||
let path = std::path::Path::new(".");
|
||||
let commit_id = get_current_commit_id(path).await.unwrap_or_default();
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Eval Completed",
|
||||
cohort_id = cohort_id,
|
||||
@@ -353,7 +354,8 @@ async fn run_example(
|
||||
model_provider = model.provider_id().to_string(),
|
||||
repository_url = example.base.url.clone(),
|
||||
repository_revision = example.base.revision.clone(),
|
||||
diagnostics_summary = run_output.diagnostics
|
||||
diagnostics_summary = run_output.diagnostics,
|
||||
commit_id = commit_id
|
||||
);
|
||||
}
|
||||
|
||||
@@ -476,6 +478,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
assistant_tools::init(client.http_client().clone(), cx);
|
||||
context_server::init(cx);
|
||||
prompt_store::init(cx);
|
||||
let stdout_is_a_pty = false;
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
|
||||
agent::init(fs.clone(), client.clone(), prompt_builder.clone(), cx);
|
||||
@@ -524,3 +527,13 @@ pub fn authenticate_model_provider(
|
||||
let model_provider = model_registry.provider(&provider_id).unwrap();
|
||||
model_provider.authenticate(cx)
|
||||
}
|
||||
|
||||
pub async fn get_current_commit_id(repo_path: &Path) -> Option<String> {
|
||||
(run_git(repo_path, &["rev-parse", "HEAD"]).await).ok()
|
||||
}
|
||||
|
||||
pub fn get_current_commit_id_sync(repo_path: &Path) -> String {
|
||||
futures::executor::block_on(async {
|
||||
get_current_commit_id(repo_path).await.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ impl Example {
|
||||
return Err(anyhow!("Setup only mode"));
|
||||
}
|
||||
|
||||
let thread_store = thread_store.await;
|
||||
let thread_store = thread_store.await?;
|
||||
let thread =
|
||||
thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx))?;
|
||||
|
||||
@@ -418,7 +418,8 @@ impl Example {
|
||||
ThreadEvent::MessageDeleted(_) |
|
||||
ThreadEvent::SummaryChanged |
|
||||
ThreadEvent::SummaryGenerated |
|
||||
ThreadEvent::CheckpointChanged => {
|
||||
ThreadEvent::CheckpointChanged |
|
||||
ThreadEvent::UsageUpdated(_) => {
|
||||
if std::env::var("ZED_EVAL_DEBUG").is_ok() {
|
||||
println!("{}Event: {:#?}", log_prefix, event);
|
||||
}
|
||||
@@ -498,6 +499,8 @@ impl Example {
|
||||
)?;
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(prompt)],
|
||||
|
||||
@@ -84,6 +84,11 @@ impl FeatureFlag for ZedPro {
|
||||
const NAME: &'static str = "zed-pro";
|
||||
}
|
||||
|
||||
pub struct ZedProWebSearchTool {}
|
||||
impl FeatureFlag for ZedProWebSearchTool {
|
||||
const NAME: &'static str = "zed-pro-web-search-tool";
|
||||
}
|
||||
|
||||
pub struct NotebookFeatureFlag;
|
||||
|
||||
impl FeatureFlag for NotebookFeatureFlag {
|
||||
|
||||
@@ -604,7 +604,7 @@ impl GitPanel {
|
||||
if let Ok(ix) = self.entries[conflicted_start..conflicted_start + self.conflicted_count]
|
||||
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
|
||||
{
|
||||
return Some(ix);
|
||||
return Some(conflicted_start + ix);
|
||||
}
|
||||
}
|
||||
if self.tracked_count > 0 {
|
||||
@@ -616,7 +616,7 @@ impl GitPanel {
|
||||
if let Ok(ix) = self.entries[tracked_start..tracked_start + self.tracked_count]
|
||||
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
|
||||
{
|
||||
return Some(ix);
|
||||
return Some(tracked_start + ix);
|
||||
}
|
||||
}
|
||||
if self.new_count > 0 {
|
||||
@@ -632,7 +632,7 @@ impl GitPanel {
|
||||
if let Ok(ix) = self.entries[untracked_start..untracked_start + self.new_count]
|
||||
.binary_search_by(|entry| entry.status_entry().unwrap().repo_path.cmp(&path))
|
||||
{
|
||||
return Some(ix);
|
||||
return Some(untracked_start + ix);
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -1739,6 +1739,8 @@ impl GitPanel {
|
||||
const PROMPT: &str = include_str!("commit_message_prompt.txt");
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![content.into()],
|
||||
|
||||
@@ -125,7 +125,7 @@ pub struct GenerateContentRequest {
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub model: String,
|
||||
pub contents: Vec<Content>,
|
||||
pub system_instructions: Option<SystemInstructions>,
|
||||
pub system_instruction: Option<SystemInstruction>,
|
||||
pub generation_config: Option<GenerationConfig>,
|
||||
pub safety_settings: Option<Vec<SafetySetting>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -162,7 +162,7 @@ pub struct Content {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SystemInstructions {
|
||||
pub struct SystemInstruction {
|
||||
pub parts: Vec<Part>,
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ telemetry_events.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -8,11 +8,12 @@ mod telemetry;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod fake_provider;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, anyhow};
|
||||
use client::Client;
|
||||
use futures::FutureExt;
|
||||
use futures::{StreamExt, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use icons::IconName;
|
||||
use parking_lot::Mutex;
|
||||
use proto::Plan;
|
||||
@@ -20,9 +21,13 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use std::fmt;
|
||||
use std::ops::{Add, Sub};
|
||||
use std::str::FromStr as _;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use util::serde::is_default;
|
||||
use zed_llm_client::{
|
||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||
};
|
||||
|
||||
pub use crate::model::*;
|
||||
pub use crate::rate_limiter::*;
|
||||
@@ -83,6 +88,28 @@ pub enum StopReason {
|
||||
ToolUse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RequestUsage {
|
||||
pub limit: UsageLimit,
|
||||
pub amount: i32,
|
||||
}
|
||||
|
||||
impl RequestUsage {
|
||||
pub fn from_headers(headers: &HeaderMap<HeaderValue>) -> Result<Self> {
|
||||
let limit = headers
|
||||
.get(MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME)
|
||||
.ok_or_else(|| anyhow!("missing {MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME:?} header"))?;
|
||||
let limit = UsageLimit::from_str(limit.to_str()?)?;
|
||||
|
||||
let amount = headers
|
||||
.get(MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME)
|
||||
.ok_or_else(|| anyhow!("missing {MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME:?} header"))?;
|
||||
let amount = amount.to_str()?.parse::<i32>()?;
|
||||
|
||||
Ok(Self { limit, amount })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, Default)]
|
||||
pub struct TokenUsage {
|
||||
#[serde(default, skip_serializing_if = "is_default")]
|
||||
@@ -97,7 +124,10 @@ pub struct TokenUsage {
|
||||
|
||||
impl TokenUsage {
|
||||
pub fn total_tokens(&self) -> u32 {
|
||||
self.input_tokens + self.output_tokens
|
||||
self.input_tokens
|
||||
+ self.output_tokens
|
||||
+ self.cache_read_input_tokens
|
||||
+ self.cache_creation_input_tokens
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,15 +241,42 @@ pub trait LanguageModel: Send + Sync {
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>>;
|
||||
|
||||
fn stream_completion_with_usage(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
Result<(
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent>>,
|
||||
Option<RequestUsage>,
|
||||
)>,
|
||||
> {
|
||||
self.stream_completion(request, cx)
|
||||
.map(|result| result.map(|stream| (stream, None)))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn stream_completion_text(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<LanguageModelTextStream>> {
|
||||
let events = self.stream_completion(request, cx);
|
||||
self.stream_completion_text_with_usage(request, cx)
|
||||
.map(|result| result.map(|(stream, _usage)| stream))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn stream_completion_text_with_usage(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<(LanguageModelTextStream, Option<RequestUsage>)>> {
|
||||
let future = self.stream_completion_with_usage(request, cx);
|
||||
|
||||
async move {
|
||||
let mut events = events.await?.fuse();
|
||||
let (events, usage) = future.await?;
|
||||
let mut events = events.fuse();
|
||||
let mut message_id = None;
|
||||
let mut first_item_text = None;
|
||||
let last_token_usage = Arc::new(Mutex::new(TokenUsage::default()));
|
||||
@@ -259,11 +316,14 @@ pub trait LanguageModel: Send + Sync {
|
||||
}))
|
||||
.boxed();
|
||||
|
||||
Ok(LanguageModelTextStream {
|
||||
message_id,
|
||||
stream,
|
||||
last_token_usage,
|
||||
})
|
||||
Ok((
|
||||
LanguageModelTextStream {
|
||||
message_id,
|
||||
stream,
|
||||
last_token_usage,
|
||||
},
|
||||
usage,
|
||||
))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -90,6 +90,8 @@ impl CloudModel {
|
||||
| open_ai::Model::O1Preview
|
||||
| open_ai::Model::O1
|
||||
| open_ai::Model::O3Mini
|
||||
| open_ai::Model::O3
|
||||
| open_ai::Model::O4Mini
|
||||
| open_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
@@ -142,6 +144,27 @@ impl fmt::Display for MaxMonthlySpendReachedError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub struct ModelRequestLimitReachedError {
|
||||
pub plan: Plan,
|
||||
}
|
||||
|
||||
impl fmt::Display for ModelRequestLimitReachedError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let message = match self.plan {
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
Plan::ZedProTrial => {
|
||||
"Model request limit reached. Upgrade to Zed Pro for more requests."
|
||||
}
|
||||
};
|
||||
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use crate::RequestUsage;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RateLimiter {
|
||||
semaphore: Arc<Semaphore>,
|
||||
@@ -67,4 +69,32 @@ impl RateLimiter {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stream_with_usage<'a, Fut, T>(
|
||||
&self,
|
||||
future: Fut,
|
||||
) -> impl 'a
|
||||
+ Future<
|
||||
Output = Result<(
|
||||
impl Stream<Item = T::Item> + use<Fut, T>,
|
||||
Option<RequestUsage>,
|
||||
)>,
|
||||
>
|
||||
where
|
||||
Fut: 'a + Future<Output = Result<(T, Option<RequestUsage>)>>,
|
||||
T: Stream,
|
||||
{
|
||||
let guard = self.semaphore.acquire_arc();
|
||||
async move {
|
||||
let guard = guard.await;
|
||||
let (inner, usage) = future.await?;
|
||||
Ok((
|
||||
RateLimitGuard {
|
||||
inner,
|
||||
_guard: guard,
|
||||
},
|
||||
usage,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +238,8 @@ pub struct LanguageModelRequestTool {
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LanguageModelRequest {
|
||||
pub thread_id: Option<String>,
|
||||
pub prompt_id: Option<String>,
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
pub tools: Vec<LanguageModelRequestTool>,
|
||||
pub stop: Vec<String>,
|
||||
|
||||
@@ -546,7 +546,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
|
||||
let plan = proto::Plan::ZedPro;
|
||||
let is_trial = false;
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
@@ -558,7 +557,6 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
.justify_between()
|
||||
.when(cx.has_flag::<ZedPro>(), |this| {
|
||||
this.child(match plan {
|
||||
// Already a Zed Pro subscriber
|
||||
Plan::ZedPro => Button::new("zed-pro", "Zed Pro")
|
||||
.icon(IconName::ZedAssistant)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -568,10 +566,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
window
|
||||
.dispatch_action(Box::new(zed_actions::OpenAccountSettings), cx)
|
||||
}),
|
||||
// Free user
|
||||
Plan::Free => Button::new(
|
||||
Plan::Free | Plan::ZedProTrial => Button::new(
|
||||
"try-pro",
|
||||
if is_trial {
|
||||
if plan == Plan::ZedProTrial {
|
||||
"Upgrade to Pro"
|
||||
} else {
|
||||
"Try Pro"
|
||||
|
||||
@@ -53,6 +53,7 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -705,12 +705,12 @@ pub fn map_to_language_model_completion_events(
|
||||
update_usage(&mut state.usage, &message.usage);
|
||||
return Some((
|
||||
vec![
|
||||
Ok(LanguageModelCompletionEvent::StartMessage {
|
||||
message_id: message.id,
|
||||
}),
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(convert_usage(
|
||||
&state.usage,
|
||||
))),
|
||||
Ok(LanguageModelCompletionEvent::StartMessage {
|
||||
message_id: message.id,
|
||||
}),
|
||||
],
|
||||
state,
|
||||
));
|
||||
@@ -934,7 +934,7 @@ impl Render for ConfigurationView {
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
@@ -948,8 +948,13 @@ impl Render for ConfigurationView {
|
||||
.into_any()
|
||||
} else {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -961,7 +966,8 @@ impl Render for ConfigurationView {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-key", "Reset key")
|
||||
Button::new("reset-key", "Reset Key")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -1145,7 +1145,7 @@ impl ConfigurationView {
|
||||
|
||||
fn make_input_styles(&self, cx: &Context<Self>) -> Div {
|
||||
let bg_color = cx.theme().colors().editor_background;
|
||||
let border_color = cx.theme().colors().border_variant;
|
||||
let border_color = cx.theme().colors().border;
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
@@ -1173,8 +1173,13 @@ impl Render for ConfigurationView {
|
||||
|
||||
if let Some(auth) = self.should_render_editor(cx) {
|
||||
return h_flex()
|
||||
.size_full()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -1186,16 +1191,16 @@ impl Render for ConfigurationView {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-key", "Reset key")
|
||||
Button::new("reset-key", "Reset Key")
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(env_var_set || creds_type)
|
||||
// .disabled(env_var_set || creds_type)
|
||||
.when(env_var_set, |this| {
|
||||
this.tooltip(Tooltip::text(format!("To reset your credentials, unset the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR}, and {ZED_BEDROCK_REGION_VAR} environment variables.")))
|
||||
})
|
||||
.when(creds_type, |this| {
|
||||
this.tooltip(Tooltip::text("You cannot reset credentials as they're being derived, check Zed settings to understand how"))
|
||||
this.tooltip(Tooltip::text("You cannot reset credentials as they're being derived, check Zed settings to understand how."))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| this.reset_credentials(window, cx))),
|
||||
)
|
||||
@@ -1206,19 +1211,19 @@ impl Render for ConfigurationView {
|
||||
.size_full()
|
||||
.on_action(cx.listener(ConfigurationView::save_credentials))
|
||||
.child(Label::new("To use Zed's assistant with Bedrock, you can set a custom authentication strategy through the settings.json, or use static credentials."))
|
||||
.child(Label::new("Though to access models on AWS first, you will have to: "))
|
||||
.child(Label::new("But, to access models on AWS, you need to:").mt_1())
|
||||
.child(
|
||||
List::new()
|
||||
.child(
|
||||
InstructionListItem::new(
|
||||
"Grant permissions to the strategy you plan to use according to this documentation: ",
|
||||
"Grant permissions to the strategy you'll use according to the:",
|
||||
Some("Prerequisites"),
|
||||
Some("https://docs.aws.amazon.com/bedrock/latest/userguide/inference-prereq.html"),
|
||||
)
|
||||
)
|
||||
.child(
|
||||
InstructionListItem::new(
|
||||
"Select the models you would like access to: ",
|
||||
"Select the models you would like access to:",
|
||||
Some("Bedrock Model Catalog"),
|
||||
Some("https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess"),
|
||||
)
|
||||
@@ -1228,7 +1233,15 @@ impl Render for ConfigurationView {
|
||||
.child(self.render_common_fields(cx))
|
||||
.child(
|
||||
Label::new(
|
||||
format!("You can also assign the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR} AND {ZED_BEDROCK_REGION_VAR} environment variables and restart Zed.\n Optionally, if your environment uses AWS CLI profiles, you can set {ZED_AWS_PROFILE_VAR}; if it requires a custom endpoint, you can set {ZED_AWS_ENDPOINT_VAR}; and if it requires a Session Token, you can set {ZED_BEDROCK_SESSION_TOKEN_VAR}."),
|
||||
format!("You can also assign the {ZED_BEDROCK_ACCESS_KEY_ID_VAR}, {ZED_BEDROCK_SECRET_ACCESS_KEY_VAR} AND {ZED_BEDROCK_REGION_VAR} environment variables and restart Zed."),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.my_1(),
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
format!("Optionally, if your environment uses AWS CLI profiles, you can set {ZED_AWS_PROFILE_VAR}; if it requires a custom endpoint, you can set {ZED_AWS_ENDPOINT_VAR}; and if it requires a Session Token, you can set {ZED_BEDROCK_SESSION_TOKEN_VAR}."),
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
@@ -1307,7 +1320,6 @@ impl ConfigurationView {
|
||||
Label::new(
|
||||
"This method uses your AWS access key ID and secret access key directly.",
|
||||
)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
List::new()
|
||||
@@ -1357,16 +1369,11 @@ impl ConfigurationView {
|
||||
|
||||
fn render_common_fields(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
v_flex()
|
||||
.my_2()
|
||||
.gap_1p5()
|
||||
.gap_0p5()
|
||||
.child(Label::new("Region").size(LabelSize::Small))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(Label::new("Region").size(LabelSize::Small))
|
||||
.child(
|
||||
self.make_input_styles(cx)
|
||||
.child(self.render_region_editor(cx)),
|
||||
),
|
||||
self.make_input_styles(cx)
|
||||
.child(self.render_region_editor(cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use anthropic::{AnthropicError, AnthropicModelMode, parse_prompt_too_long};
|
||||
use anyhow::{Result, anyhow};
|
||||
use client::{
|
||||
Client, EXPIRED_LLM_TOKEN_HEADER_NAME, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME,
|
||||
PerformCompletionParams, UserStore, zed_urls,
|
||||
};
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use collections::BTreeMap;
|
||||
use feature_flags::{FeatureFlagAppExt, LlmClosedBeta, ZedPro};
|
||||
use futures::{
|
||||
@@ -16,18 +13,20 @@ use language_model::{
|
||||
AuthenticateError, CloudModel, LanguageModel, LanguageModelCacheConfiguration, LanguageModelId,
|
||||
LanguageModelKnownError, LanguageModelName, LanguageModelProviderId, LanguageModelProviderName,
|
||||
LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
|
||||
LanguageModelToolSchemaFormat, RateLimiter, ZED_CLOUD_PROVIDER_ID,
|
||||
LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter, RequestUsage,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelAvailability, LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken,
|
||||
MaxMonthlySpendReachedError, PaymentRequiredError, RefreshLlmTokenListener,
|
||||
};
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde_json::value::RawValue;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::Timer;
|
||||
use smol::io::{AsyncReadExt, BufReader};
|
||||
use std::str::FromStr as _;
|
||||
use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
@@ -35,6 +34,11 @@ use std::{
|
||||
use strum::IntoEnumIterator;
|
||||
use thiserror::Error;
|
||||
use ui::{TintColor, prelude::*};
|
||||
use zed_llm_client::{
|
||||
CURRENT_PLAN_HEADER_NAME, CompletionBody, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
||||
SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||
};
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::provider::anthropic::{count_anthropic_tokens, into_anthropic};
|
||||
@@ -513,8 +517,8 @@ impl CloudLanguageModel {
|
||||
async fn perform_llm_completion(
|
||||
client: Arc<Client>,
|
||||
llm_api_token: LlmApiToken,
|
||||
body: PerformCompletionParams,
|
||||
) -> Result<Response<AsyncBody>> {
|
||||
body: CompletionBody,
|
||||
) -> Result<(Response<AsyncBody>, Option<RequestUsage>)> {
|
||||
let http_client = &client.http_client();
|
||||
|
||||
let mut token = llm_api_token.acquire(&client).await?;
|
||||
@@ -536,7 +540,9 @@ impl CloudLanguageModel {
|
||||
let mut response = http_client.send(request).await?;
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
return Ok(response);
|
||||
let usage = RequestUsage::from_headers(response.headers()).ok();
|
||||
|
||||
return Ok((response, usage));
|
||||
} else if response
|
||||
.headers()
|
||||
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
|
||||
@@ -551,6 +557,33 @@ impl CloudLanguageModel {
|
||||
.is_some()
|
||||
{
|
||||
return Err(anyhow!(MaxMonthlySpendReachedError));
|
||||
} else if status == StatusCode::FORBIDDEN
|
||||
&& response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
if let Some(MODEL_REQUESTS_RESOURCE_HEADER_VALUE) = response
|
||||
.headers()
|
||||
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
|
||||
.and_then(|resource| resource.to_str().ok())
|
||||
{
|
||||
if let Some(plan) = response
|
||||
.headers()
|
||||
.get(CURRENT_PLAN_HEADER_NAME)
|
||||
.and_then(|plan| plan.to_str().ok())
|
||||
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
|
||||
{
|
||||
let plan = match plan {
|
||||
zed_llm_client::Plan::Free => Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
zed_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
};
|
||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(anyhow!("Forbidden"));
|
||||
} else if status.as_u16() >= 500 && status.as_u16() < 600 {
|
||||
// If we encounter an error in the 500 range, retry after a delay.
|
||||
// We've seen at least these in the wild from API providers:
|
||||
@@ -677,8 +710,26 @@ impl LanguageModel for CloudLanguageModel {
|
||||
fn stream_completion(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
_cx: &AsyncApp,
|
||||
cx: &AsyncApp,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {
|
||||
self.stream_completion_with_usage(request, cx)
|
||||
.map(|result| result.map(|(stream, _)| stream))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn stream_completion_with_usage(
|
||||
&self,
|
||||
request: LanguageModelRequest,
|
||||
_cx: &AsyncApp,
|
||||
) -> BoxFuture<
|
||||
'static,
|
||||
Result<(
|
||||
BoxStream<'static, Result<LanguageModelCompletionEvent>>,
|
||||
Option<RequestUsage>,
|
||||
)>,
|
||||
> {
|
||||
let thread_id = request.thread_id.clone();
|
||||
let prompt_id = request.prompt_id.clone();
|
||||
match &self.model {
|
||||
CloudModel::Anthropic(model) => {
|
||||
let request = into_anthropic(
|
||||
@@ -690,16 +741,16 @@ impl LanguageModel for CloudLanguageModel {
|
||||
);
|
||||
let client = self.client.clone();
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = Self::perform_llm_completion(
|
||||
let future = self.request_limiter.stream_with_usage(async move {
|
||||
let (response, usage) = Self::perform_llm_completion(
|
||||
client.clone(),
|
||||
llm_api_token,
|
||||
PerformCompletionParams {
|
||||
provider: client::LanguageModelProvider::Anthropic,
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
provider_request: RawValue::from_string(serde_json::to_string(
|
||||
&request,
|
||||
)?)?,
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
},
|
||||
)
|
||||
.await
|
||||
@@ -719,63 +770,78 @@ impl LanguageModel for CloudLanguageModel {
|
||||
Err(err) => anyhow!(err),
|
||||
})?;
|
||||
|
||||
Ok(
|
||||
Ok((
|
||||
crate::provider::anthropic::map_to_language_model_completion_events(
|
||||
Box::pin(response_lines(response).map_err(AnthropicError::Other)),
|
||||
),
|
||||
)
|
||||
usage,
|
||||
))
|
||||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
async move {
|
||||
let (stream, usage) = future.await?;
|
||||
Ok((stream.boxed(), usage))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
CloudModel::OpenAi(model) => {
|
||||
let client = self.client.clone();
|
||||
let request = into_open_ai(request, model, model.max_output_tokens());
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = Self::perform_llm_completion(
|
||||
let future = self.request_limiter.stream_with_usage(async move {
|
||||
let (response, usage) = Self::perform_llm_completion(
|
||||
client.clone(),
|
||||
llm_api_token,
|
||||
PerformCompletionParams {
|
||||
provider: client::LanguageModelProvider::OpenAi,
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
provider_request: RawValue::from_string(serde_json::to_string(
|
||||
&request,
|
||||
)?)?,
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
Ok((
|
||||
crate::provider::open_ai::map_to_language_model_completion_events(
|
||||
Box::pin(response_lines(response)),
|
||||
),
|
||||
)
|
||||
usage,
|
||||
))
|
||||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
async move {
|
||||
let (stream, usage) = future.await?;
|
||||
Ok((stream.boxed(), usage))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
CloudModel::Google(model) => {
|
||||
let client = self.client.clone();
|
||||
let request = into_google(request, model.id().into());
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let future = self.request_limiter.stream(async move {
|
||||
let response = Self::perform_llm_completion(
|
||||
let future = self.request_limiter.stream_with_usage(async move {
|
||||
let (response, usage) = Self::perform_llm_completion(
|
||||
client.clone(),
|
||||
llm_api_token,
|
||||
PerformCompletionParams {
|
||||
provider: client::LanguageModelProvider::Google,
|
||||
CompletionBody {
|
||||
thread_id,
|
||||
prompt_id,
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
model: request.model.clone(),
|
||||
provider_request: RawValue::from_string(serde_json::to_string(
|
||||
&request,
|
||||
)?)?,
|
||||
provider_request: serde_json::to_value(&request)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(
|
||||
Ok((
|
||||
crate::provider::google::map_to_language_model_completion_events(Box::pin(
|
||||
response_lines(response),
|
||||
)),
|
||||
)
|
||||
usage,
|
||||
))
|
||||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
async move {
|
||||
let (stream, usage) = future.await?;
|
||||
Ok((stream.boxed(), usage))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,10 @@ impl LanguageModel for CopilotChatLanguageModel {
|
||||
CopilotChatModel::Gpt4 => open_ai::Model::Four,
|
||||
CopilotChatModel::Gpt4_1 => open_ai::Model::FourPointOne,
|
||||
CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,
|
||||
CopilotChatModel::O1 | CopilotChatModel::O3Mini => open_ai::Model::Four,
|
||||
CopilotChatModel::O1 => open_ai::Model::O1,
|
||||
CopilotChatModel::O3Mini => open_ai::Model::O3Mini,
|
||||
CopilotChatModel::O3 => open_ai::Model::O3,
|
||||
CopilotChatModel::O4Mini => open_ai::Model::O4Mini,
|
||||
CopilotChatModel::Claude3_5Sonnet
|
||||
| CopilotChatModel::Claude3_7Sonnet
|
||||
| CopilotChatModel::Claude3_7SonnetThinking
|
||||
@@ -527,60 +530,51 @@ impl ConfigurationView {
|
||||
}
|
||||
|
||||
impl Render for ConfigurationView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.state.read(cx).is_authenticated(cx) {
|
||||
const LABEL: &str = "Authorized.";
|
||||
h_flex()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Check).color(Color::Success))
|
||||
.child(Label::new(LABEL)),
|
||||
.child(Label::new("Authorized")),
|
||||
)
|
||||
.child(
|
||||
Button::new("sign_out", "Sign Out")
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(copilot::SignOut.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
let loading_icon = svg()
|
||||
.size_8()
|
||||
.path(IconName::ArrowCircle.path())
|
||||
.text_color(window.text_style().color)
|
||||
.with_animation(
|
||||
"icon_circle_arrow",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
|
||||
);
|
||||
let loading_icon = Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
);
|
||||
|
||||
const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider.";
|
||||
|
||||
match &self.copilot_status {
|
||||
Some(status) => match status {
|
||||
Status::Starting { task: _ } => {
|
||||
const LABEL: &str = "Starting Copilot...";
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon)
|
||||
}
|
||||
Status::Starting { task: _ } => h_flex()
|
||||
.gap_2()
|
||||
.child(loading_icon)
|
||||
.child(Label::new("Starting Copilot…")),
|
||||
Status::SigningIn { prompt: _ }
|
||||
| Status::SignedOut {
|
||||
awaiting_signing_in: true,
|
||||
} => {
|
||||
const LABEL: &str = "Signing in to Copilot...";
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new(LABEL))
|
||||
.child(loading_icon)
|
||||
}
|
||||
} => h_flex()
|
||||
.gap_2()
|
||||
.child(loading_icon)
|
||||
.child(Label::new("Signing into Copilot…")),
|
||||
Status::Error(_) => {
|
||||
const LABEL: &str = "Copilot had issues starting. Please try restarting it. If the issue persists, try reinstalling Copilot.";
|
||||
v_flex()
|
||||
@@ -590,28 +584,14 @@ impl Render for ConfigurationView {
|
||||
}
|
||||
_ => {
|
||||
const LABEL: &str = "To use Zed's assistant with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
|
||||
v_flex().gap_6().child(Label::new(LABEL)).child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("sign_in", "Sign In")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Medium)
|
||||
.style(ui::ButtonStyle::Filled)
|
||||
.full_width()
|
||||
.on_click(|_, window, cx| {
|
||||
copilot::initiate_sign_in(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
Label::new("Sign in to start using Github Copilot Chat.")
|
||||
.color(Color::Muted)
|
||||
.size(ui::LabelSize::Small),
|
||||
),
|
||||
),
|
||||
v_flex().gap_2().child(Label::new(LABEL)).child(
|
||||
Button::new("sign_in", "Sign in to use GitHub Copilot")
|
||||
.icon_color(Color::Muted)
|
||||
.icon(IconName::Github)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Medium)
|
||||
.full_width()
|
||||
.on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -580,7 +580,7 @@ impl Render for ConfigurationView {
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
@@ -595,8 +595,13 @@ impl Render for ConfigurationView {
|
||||
.into_any()
|
||||
} else {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -608,8 +613,11 @@ impl Render for ConfigurationView {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-key", "Reset")
|
||||
.icon(IconName::Trash)
|
||||
Button::new("reset-key", "Reset Key")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(env_var_set)
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.reset_api_key(window, cx)),
|
||||
|
||||
@@ -4,7 +4,7 @@ use credentials_provider::CredentialsProvider;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use futures::{FutureExt, Stream, StreamExt, future::BoxFuture};
|
||||
use google_ai::{
|
||||
FunctionDeclaration, GenerateContentResponse, Part, SystemInstructions, UsageMetadata,
|
||||
FunctionDeclaration, GenerateContentResponse, Part, SystemInstruction, UsageMetadata,
|
||||
};
|
||||
use gpui::{
|
||||
AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
|
||||
@@ -405,7 +405,7 @@ pub fn into_google(
|
||||
.map_or(false, |msg| matches!(msg.role, Role::System))
|
||||
{
|
||||
let message = request.messages.remove(0);
|
||||
Some(SystemInstructions {
|
||||
Some(SystemInstruction {
|
||||
parts: map_content(message.content),
|
||||
})
|
||||
} else {
|
||||
@@ -414,7 +414,7 @@ pub fn into_google(
|
||||
|
||||
google_ai::GenerateContentRequest {
|
||||
model,
|
||||
system_instructions,
|
||||
system_instruction: system_instructions,
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
@@ -740,7 +740,7 @@ impl Render for ConfigurationView {
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
@@ -753,8 +753,13 @@ impl Render for ConfigurationView {
|
||||
.into_any()
|
||||
} else {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -766,7 +771,8 @@ impl Render for ConfigurationView {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-key", "Reset key")
|
||||
Button::new("reset-key", "Reset Key")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -16,10 +16,11 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use ui::{ButtonLike, Indicator, prelude::*};
|
||||
use ui::{ButtonLike, Indicator, List, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::ui::InstructionListItem;
|
||||
|
||||
const LMSTUDIO_DOWNLOAD_URL: &str = "https://lmstudio.ai/download";
|
||||
const LMSTUDIO_CATALOG_URL: &str = "https://lmstudio.ai/models";
|
||||
@@ -408,40 +409,26 @@ impl Render for ConfigurationView {
|
||||
let is_authenticated = self.state.read(cx).is_authenticated();
|
||||
|
||||
let lmstudio_intro = "Run local LLMs like Llama, Phi, and Qwen.";
|
||||
let lmstudio_reqs = "To use LM Studio as a provider for Zed assistant, it needs to be running with at least one model downloaded.";
|
||||
|
||||
let inline_code_bg = cx.theme().colors().editor_foreground.opacity(0.05);
|
||||
|
||||
if self.loading_models_task.is_some() {
|
||||
div().child(Label::new("Loading models...")).into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_3()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_1()
|
||||
.child(Label::new(lmstudio_intro))
|
||||
.child(Label::new(lmstudio_reqs))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Label::new("To get your first model, try running"))
|
||||
.child(
|
||||
div()
|
||||
.bg(inline_code_bg)
|
||||
.px_1p5()
|
||||
.rounded_sm()
|
||||
.child(Label::new("lms get qwen2.5-coder-7b")),
|
||||
),
|
||||
),
|
||||
v_flex().gap_1().child(Label::new(lmstudio_intro)).child(
|
||||
List::new()
|
||||
.child(InstructionListItem::text_only(
|
||||
"LM Studio needs to be running with at least one model downloaded.",
|
||||
))
|
||||
.child(InstructionListItem::text_only(
|
||||
"To get your first model, try running `lms get qwen2.5-coder-7b`",
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pt_2()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
@@ -489,29 +476,31 @@ impl Render for ConfigurationView {
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(if is_authenticated {
|
||||
// This is only a button to ensure the spacing is correct
|
||||
// it should stay disabled
|
||||
ButtonLike::new("connected")
|
||||
.disabled(true)
|
||||
// Since this won't ever be clickable, we can use the arrow cursor
|
||||
.cursor_style(gpui::CursorStyle::Arrow)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new("Connected"))
|
||||
.into_any_element(),
|
||||
.map(|this| {
|
||||
if is_authenticated {
|
||||
this.child(
|
||||
ButtonLike::new("connected")
|
||||
.disabled(true)
|
||||
.cursor_style(gpui::CursorStyle::Arrow)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new("Connected"))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Button::new("retry_lmstudio_models", "Connect")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::ArrowCircle)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.retry_connection(cx)
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
this.child(
|
||||
Button::new("retry_lmstudio_models", "Connect")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon(IconName::Play)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.retry_connection(cx)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
|
||||
@@ -554,7 +554,7 @@ impl Render for ConfigurationView {
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
@@ -567,8 +567,13 @@ impl Render for ConfigurationView {
|
||||
.into_any()
|
||||
} else {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -580,7 +585,8 @@ impl Render for ConfigurationView {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-key", "Reset key")
|
||||
Button::new("reset-key", "Reset Key")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -16,10 +16,11 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
use ui::{ButtonLike, Indicator, prelude::*};
|
||||
use ui::{ButtonLike, Indicator, List, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::AllLanguageModelSettings;
|
||||
use crate::ui::InstructionListItem;
|
||||
|
||||
const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
|
||||
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
|
||||
@@ -399,42 +400,26 @@ impl Render for ConfigurationView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_authenticated = self.state.read(cx).is_authenticated();
|
||||
|
||||
let ollama_intro = "Get up and running with Llama 3.3, Mistral, Gemma 2, and other large language models with Ollama.";
|
||||
let ollama_reqs =
|
||||
"Ollama must be running with at least one model installed to use it in the assistant.";
|
||||
|
||||
let inline_code_bg = cx.theme().colors().editor_foreground.opacity(0.05);
|
||||
let ollama_intro =
|
||||
"Get up & running with Llama 3.3, Mistral, Gemma 2, and other LLMs with Ollama.";
|
||||
|
||||
if self.loading_models_task.is_some() {
|
||||
div().child(Label::new("Loading models...")).into_any()
|
||||
} else {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_3()
|
||||
.gap_2()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_2()
|
||||
.p_1()
|
||||
.child(Label::new(ollama_intro))
|
||||
.child(Label::new(ollama_reqs))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(Label::new("Once installed, try "))
|
||||
.child(
|
||||
div()
|
||||
.bg(inline_code_bg)
|
||||
.px_1p5()
|
||||
.rounded_sm()
|
||||
.child(Label::new("ollama run llama3.2")),
|
||||
),
|
||||
),
|
||||
v_flex().gap_1().child(Label::new(ollama_intro)).child(
|
||||
List::new()
|
||||
.child(InstructionListItem::text_only("Ollama must be running with at least one model installed to use it in the assistant."))
|
||||
.child(InstructionListItem::text_only(
|
||||
"Once installed, try `ollama run llama3.2`",
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pt_2()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
@@ -478,30 +463,32 @@ impl Render for ConfigurationView {
|
||||
.on_click(move |_, _, cx| cx.open_url(OLLAMA_LIBRARY_URL)),
|
||||
),
|
||||
)
|
||||
.child(if is_authenticated {
|
||||
// This is only a button to ensure the spacing is correct
|
||||
// it should stay disabled
|
||||
ButtonLike::new("connected")
|
||||
.disabled(true)
|
||||
// Since this won't ever be clickable, we can use the arrow cursor
|
||||
.cursor_style(gpui::CursorStyle::Arrow)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new("Connected"))
|
||||
.into_any_element(),
|
||||
.map(|this| {
|
||||
if is_authenticated {
|
||||
this.child(
|
||||
ButtonLike::new("connected")
|
||||
.disabled(true)
|
||||
.cursor_style(gpui::CursorStyle::Arrow)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Indicator::dot().color(Color::Success))
|
||||
.child(Label::new("Connected"))
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
Button::new("retry_ollama_models", "Connect")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::ArrowCircle)
|
||||
.on_click(
|
||||
cx.listener(move |this, _, _, cx| this.retry_connection(cx)),
|
||||
} else {
|
||||
this.child(
|
||||
Button::new("retry_ollama_models", "Connect")
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon(IconName::Play)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.retry_connection(cx)
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}),
|
||||
}
|
||||
})
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -693,7 +693,7 @@ impl Render for ConfigurationView {
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_sm()
|
||||
.child(self.render_api_key_editor(cx)),
|
||||
)
|
||||
@@ -712,8 +712,13 @@ impl Render for ConfigurationView {
|
||||
.into_any()
|
||||
} else {
|
||||
h_flex()
|
||||
.size_full()
|
||||
.mt_1()
|
||||
.p_1()
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -725,7 +730,8 @@ impl Render for ConfigurationView {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-key", "Reset key")
|
||||
Button::new("reset-key", "Reset Key")
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(Some(IconName::Trash))
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_position(IconPosition::Start)
|
||||
|
||||
@@ -107,6 +107,7 @@ struct Options {
|
||||
pub enum CodeBlockRenderer {
|
||||
Default {
|
||||
copy_button: bool,
|
||||
border: bool,
|
||||
},
|
||||
Custom {
|
||||
render: CodeBlockRenderFn,
|
||||
@@ -381,7 +382,10 @@ impl MarkdownElement {
|
||||
Self {
|
||||
markdown,
|
||||
style,
|
||||
code_block_renderer: CodeBlockRenderer::Default { copy_button: true },
|
||||
code_block_renderer: CodeBlockRenderer::Default {
|
||||
copy_button: true,
|
||||
border: false,
|
||||
},
|
||||
on_url_click: None,
|
||||
}
|
||||
}
|
||||
@@ -748,6 +752,16 @@ impl Element for MarkdownElement {
|
||||
code_block.w_full()
|
||||
}
|
||||
});
|
||||
|
||||
if let CodeBlockRenderer::Default { border: true, .. } =
|
||||
&self.code_block_renderer
|
||||
{
|
||||
code_block = code_block
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant);
|
||||
}
|
||||
|
||||
code_block.style().refine(&self.style.code_block);
|
||||
if let Some(code_block_text_style) = &self.style.code_block.text
|
||||
{
|
||||
@@ -947,10 +961,10 @@ impl Element for MarkdownElement {
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(
|
||||
&self.code_block_renderer,
|
||||
CodeBlockRenderer::Default { copy_button: true }
|
||||
) {
|
||||
if let CodeBlockRenderer::Default {
|
||||
copy_button: true, ..
|
||||
} = &self.code_block_renderer
|
||||
{
|
||||
builder.flush_text();
|
||||
builder.modify_current_div(|el| {
|
||||
let content_range = parser::extract_code_block_content_range(
|
||||
|
||||
@@ -71,7 +71,7 @@ impl Anchor {
|
||||
if self_excerpt_id == ExcerptId::min() || self_excerpt_id == ExcerptId::max() {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) {
|
||||
let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer);
|
||||
if text_cmp.is_ne() {
|
||||
return text_cmp;
|
||||
|
||||
@@ -5170,6 +5170,7 @@ impl MultiBufferSnapshot {
|
||||
excerpt_id: ExcerptId,
|
||||
text_anchor: text::Anchor,
|
||||
) -> Option<Anchor> {
|
||||
let excerpt_id = self.latest_excerpt_id(excerpt_id);
|
||||
let locator = self.excerpt_locator_for_id(excerpt_id);
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
cursor.seek(locator, Bias::Left, &());
|
||||
@@ -6041,7 +6042,7 @@ impl MultiBufferSnapshot {
|
||||
return &entry.locator;
|
||||
}
|
||||
}
|
||||
panic!("invalid excerpt id {:?}", id)
|
||||
panic!("invalid excerpt id {id:?}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ pub enum Model {
|
||||
O1Mini,
|
||||
#[serde(rename = "o3-mini", alias = "o3-mini")]
|
||||
O3Mini,
|
||||
#[serde(rename = "o3", alias = "o3")]
|
||||
O3,
|
||||
#[serde(rename = "o4-mini", alias = "o4-mini")]
|
||||
O4Mini,
|
||||
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
@@ -112,6 +116,8 @@ impl Model {
|
||||
"o1-preview" => Ok(Self::O1Preview),
|
||||
"o1-mini" => Ok(Self::O1Mini),
|
||||
"o3-mini" => Ok(Self::O3Mini),
|
||||
"o3" => Ok(Self::O3),
|
||||
"o4-mini" => Ok(Self::O4Mini),
|
||||
_ => Err(anyhow!("invalid model id")),
|
||||
}
|
||||
}
|
||||
@@ -130,6 +136,8 @@ impl Model {
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O3 => "o3",
|
||||
Self::O4Mini => "o4-mini",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -148,6 +156,8 @@ impl Model {
|
||||
Self::O1Preview => "o1-preview",
|
||||
Self::O1Mini => "o1-mini",
|
||||
Self::O3Mini => "o3-mini",
|
||||
Self::O3 => "o3",
|
||||
Self::O4Mini => "o4-mini",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -168,6 +178,8 @@ impl Model {
|
||||
Self::O1Preview => 128_000,
|
||||
Self::O1Mini => 128_000,
|
||||
Self::O3Mini => 200_000,
|
||||
Self::O3 => 200_000,
|
||||
Self::O4Mini => 200_000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +531,31 @@ impl LspCommand for GetLspRunnables {
|
||||
task_template.args.extend(cargo.cargo_args);
|
||||
if !cargo.executable_args.is_empty() {
|
||||
task_template.args.push("--".to_string());
|
||||
task_template.args.extend(cargo.executable_args);
|
||||
task_template.args.extend(
|
||||
cargo
|
||||
.executable_args
|
||||
.into_iter()
|
||||
// rust-analyzer's doctest data may be smth. like
|
||||
// ```
|
||||
// command: "cargo",
|
||||
// args: [
|
||||
// "test",
|
||||
// "--doc",
|
||||
// "--package",
|
||||
// "cargo-output-parser",
|
||||
// "--",
|
||||
// "X<T>::new",
|
||||
// "--show-output",
|
||||
// ],
|
||||
// ```
|
||||
// and `X<T>::new` will cause troubles if not escaped properly, as later
|
||||
// the task runs as `$SHELL -i -c "cargo test ..."`.
|
||||
//
|
||||
// We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`.
|
||||
// That bit is not auto-expanded when using single quotes.
|
||||
// Escape extra cargo args unconditionally as those are unlikely to contain `~`.
|
||||
.map(|extra_arg| format!("'{extra_arg}'")),
|
||||
);
|
||||
}
|
||||
}
|
||||
RunnableArgs::Shell(shell) => {
|
||||
|
||||
@@ -29,6 +29,6 @@ settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -75,6 +75,7 @@ pub fn open_prompt_library(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
||||
make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>,
|
||||
prompt_to_focus: Option<PromptId>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<WindowHandle<PromptLibrary>>> {
|
||||
let store = PromptStore::global(cx);
|
||||
@@ -88,7 +89,12 @@ pub fn open_prompt_library(
|
||||
.find_map(|window| window.downcast::<PromptLibrary>());
|
||||
if let Some(existing_window) = existing_window {
|
||||
existing_window
|
||||
.update(cx, |_, window, _| window.activate_window())
|
||||
.update(cx, |prompt_library, window, cx| {
|
||||
if let Some(prompt_to_focus) = prompt_to_focus {
|
||||
prompt_library.load_prompt(prompt_to_focus, true, window, cx);
|
||||
}
|
||||
window.activate_window()
|
||||
})
|
||||
.ok();
|
||||
|
||||
Some(existing_window)
|
||||
@@ -120,14 +126,18 @@ pub fn open_prompt_library(
|
||||
},
|
||||
|window, cx| {
|
||||
cx.new(|cx| {
|
||||
PromptLibrary::new(
|
||||
let mut prompt_library = PromptLibrary::new(
|
||||
store,
|
||||
language_registry,
|
||||
inline_assist_delegate,
|
||||
make_completion_provider,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
if let Some(prompt_to_focus) = prompt_to_focus {
|
||||
prompt_library.load_prompt(prompt_to_focus, true, window, cx);
|
||||
}
|
||||
prompt_library
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -136,7 +146,7 @@ pub fn open_prompt_library(
|
||||
}
|
||||
|
||||
pub struct PromptLibrary {
|
||||
store: Arc<PromptStore>,
|
||||
store: Entity<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
prompt_editors: HashMap<PromptId, PromptEditor>,
|
||||
active_prompt_id: Option<PromptId>,
|
||||
@@ -158,7 +168,7 @@ struct PromptEditor {
|
||||
}
|
||||
|
||||
struct PromptPickerDelegate {
|
||||
store: Arc<PromptStore>,
|
||||
store: Entity<PromptStore>,
|
||||
selected_index: usize,
|
||||
matches: Vec<PromptMetadata>,
|
||||
}
|
||||
@@ -179,8 +189,8 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.store.prompt_count() == 0 {
|
||||
fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option<SharedString> {
|
||||
let text = if self.store.read(cx).prompt_count() == 0 {
|
||||
"No prompts.".into()
|
||||
} else {
|
||||
"No prompts found matching your search.".into()
|
||||
@@ -211,7 +221,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let search = self.store.search(query);
|
||||
let search = self.store.read(cx).search(query, cx);
|
||||
let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (matches, selected_index) = cx
|
||||
@@ -339,7 +349,7 @@ impl PickerDelegate for PromptPickerDelegate {
|
||||
|
||||
impl PromptLibrary {
|
||||
fn new(
|
||||
store: Arc<PromptStore>,
|
||||
store: Entity<PromptStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
inline_assist_delegate: Box<dyn InlineAssistDelegate>,
|
||||
make_completion_provider: Arc<dyn Fn() -> Box<dyn CompletionProvider>>,
|
||||
@@ -398,7 +408,7 @@ impl PromptLibrary {
|
||||
pub fn new_prompt(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// If we already have an untitled prompt, use that instead
|
||||
// of creating a new one.
|
||||
if let Some(metadata) = self.store.first() {
|
||||
if let Some(metadata) = self.store.read(cx).first() {
|
||||
if metadata.title.is_none() {
|
||||
self.load_prompt(metadata.id, true, window, cx);
|
||||
return;
|
||||
@@ -406,7 +416,9 @@ impl PromptLibrary {
|
||||
}
|
||||
|
||||
let prompt_id = PromptId::new();
|
||||
let save = self.store.save(prompt_id, None, false, "".into());
|
||||
let save = self.store.update(cx, |store, cx| {
|
||||
store.save(prompt_id, None, false, "".into(), cx)
|
||||
});
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -430,7 +442,7 @@ impl PromptLibrary {
|
||||
return;
|
||||
}
|
||||
|
||||
let prompt_metadata = self.store.metadata(prompt_id).unwrap();
|
||||
let prompt_metadata = self.store.read(cx).metadata(prompt_id).unwrap();
|
||||
let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
|
||||
let title = prompt_editor.title_editor.read(cx).text(cx);
|
||||
let body = prompt_editor.body_editor.update(cx, |editor, cx| {
|
||||
@@ -465,10 +477,13 @@ impl PromptLibrary {
|
||||
} else {
|
||||
Some(SharedString::from(title))
|
||||
};
|
||||
store
|
||||
.save(prompt_id, title, prompt_metadata.default, body)
|
||||
.await
|
||||
.log_err();
|
||||
cx.update(|_window, cx| {
|
||||
store.update(cx, |store, cx| {
|
||||
store.save(prompt_id, title, prompt_metadata.default, body, cx)
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.log_err();
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
@@ -521,14 +536,21 @@ impl PromptLibrary {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
self.store
|
||||
.save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
|
||||
.detach_and_log_err(cx);
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
self.store.update(cx, move |store, cx| {
|
||||
if let Some(prompt_metadata) = store.metadata(prompt_id) {
|
||||
store
|
||||
.save_metadata(
|
||||
prompt_id,
|
||||
prompt_metadata.title,
|
||||
!prompt_metadata.default,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn load_prompt(
|
||||
@@ -545,9 +567,9 @@ impl PromptLibrary {
|
||||
.update(cx, |editor, cx| window.focus(&editor.focus_handle(cx)));
|
||||
}
|
||||
self.set_active_prompt(Some(prompt_id), window, cx);
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
} else if let Some(prompt_metadata) = self.store.read(cx).metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let prompt = self.store.load(prompt_id);
|
||||
let prompt = self.store.read(cx).load(prompt_id, cx);
|
||||
let make_completion_provider = self.make_completion_provider.clone();
|
||||
self.pending_load = cx.spawn_in(window, async move |this, cx| {
|
||||
let prompt = prompt.await;
|
||||
@@ -673,7 +695,7 @@ impl PromptLibrary {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(metadata) = self.store.metadata(prompt_id) {
|
||||
if let Some(metadata) = self.store.read(cx).metadata(prompt_id) {
|
||||
let confirmation = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!(
|
||||
@@ -692,7 +714,9 @@ impl PromptLibrary {
|
||||
this.set_active_prompt(None, window, cx);
|
||||
}
|
||||
this.prompt_editors.remove(&prompt_id);
|
||||
this.store.delete(prompt_id).detach_and_log_err(cx);
|
||||
this.store
|
||||
.update(cx, |store, cx| store.delete(prompt_id, cx))
|
||||
.detach_and_log_err(cx);
|
||||
this.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.notify();
|
||||
@@ -736,9 +760,9 @@ impl PromptLibrary {
|
||||
|
||||
let new_id = PromptId::new();
|
||||
let body = prompt.body_editor.read(cx).text(cx);
|
||||
let save = self
|
||||
.store
|
||||
.save(new_id, Some(title.into()), false, body.into());
|
||||
let save = self.store.update(cx, |store, cx| {
|
||||
store.save(new_id, Some(title.into()), false, body.into(), cx)
|
||||
});
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.refresh(window, cx));
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -900,6 +924,8 @@ impl PromptLibrary {
|
||||
.update(|_, cx| {
|
||||
model.count_tokens(
|
||||
LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![body.to_string().into()],
|
||||
@@ -968,7 +994,7 @@ impl PromptLibrary {
|
||||
.flex_none()
|
||||
.min_w_64()
|
||||
.children(self.active_prompt_id.and_then(|prompt_id| {
|
||||
let prompt_metadata = self.store.metadata(prompt_id)?;
|
||||
let prompt_metadata = self.store.read(cx).metadata(prompt_id)?;
|
||||
let prompt_editor = &self.prompt_editors[&prompt_id];
|
||||
let focus_handle = prompt_editor.body_editor.focus_handle(cx);
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
@@ -1238,7 +1264,7 @@ impl Render for PromptLibrary {
|
||||
.text_color(theme.colors().text)
|
||||
.child(self.render_prompt_list(cx))
|
||||
.map(|el| {
|
||||
if self.store.prompt_count() == 0 {
|
||||
if self.store.read(cx).prompt_count() == 0 {
|
||||
el.child(
|
||||
v_flex()
|
||||
.w_2_3()
|
||||
|
||||
@@ -4,9 +4,11 @@ use anyhow::{Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::future::Shared;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{App, BackgroundExecutor, Global, ReadGlobal, SharedString, Task};
|
||||
use gpui::{
|
||||
App, AppContext, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, Task,
|
||||
};
|
||||
use heed::{
|
||||
Database, RoTxn,
|
||||
types::{SerdeBincode, SerdeJson, Str},
|
||||
@@ -29,11 +31,16 @@ use uuid::Uuid;
|
||||
/// a shared future to a global.
|
||||
pub fn init(cx: &mut App) {
|
||||
let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb");
|
||||
let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())
|
||||
.then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
|
||||
.boxed()
|
||||
let prompt_store_task = PromptStore::new(db_path, cx);
|
||||
let prompt_store_entity_task = cx
|
||||
.spawn(async move |cx| {
|
||||
prompt_store_task
|
||||
.await
|
||||
.and_then(|prompt_store| cx.new(|_cx| prompt_store))
|
||||
.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
cx.set_global(GlobalPromptStore(prompt_store_future))
|
||||
cx.set_global(GlobalPromptStore(prompt_store_entity_task))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -64,13 +71,16 @@ impl PromptId {
|
||||
}
|
||||
|
||||
pub struct PromptStore {
|
||||
executor: BackgroundExecutor,
|
||||
env: heed::Env,
|
||||
metadata_cache: RwLock<MetadataCache>,
|
||||
metadata: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
|
||||
bodies: Database<SerdeJson<PromptId>, Str>,
|
||||
}
|
||||
|
||||
pub struct PromptsUpdatedEvent;
|
||||
|
||||
impl EventEmitter<PromptsUpdatedEvent> for PromptStore {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MetadataCache {
|
||||
metadata: Vec<PromptMetadata>,
|
||||
@@ -117,49 +127,45 @@ impl MetadataCache {
|
||||
}
|
||||
|
||||
impl PromptStore {
|
||||
pub fn global(cx: &App) -> impl Future<Output = Result<Arc<Self>>> + use<> {
|
||||
pub fn global(cx: &App) -> impl Future<Output = Result<Entity<Self>>> + use<> {
|
||||
let store = GlobalPromptStore::global(cx).0.clone();
|
||||
async move { store.await.map_err(|err| anyhow!(err)) }
|
||||
}
|
||||
|
||||
pub fn new(db_path: PathBuf, executor: BackgroundExecutor) -> Task<Result<Self>> {
|
||||
executor.spawn({
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
std::fs::create_dir_all(&db_path)?;
|
||||
pub fn new(db_path: PathBuf, cx: &App) -> Task<Result<Self>> {
|
||||
cx.background_spawn(async move {
|
||||
std::fs::create_dir_all(&db_path)?;
|
||||
|
||||
let db_env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024) // 1GB
|
||||
.max_dbs(4) // Metadata and bodies (possibly v1 of both as well)
|
||||
.open(db_path)?
|
||||
};
|
||||
let db_env = unsafe {
|
||||
heed::EnvOpenOptions::new()
|
||||
.map_size(1024 * 1024 * 1024) // 1GB
|
||||
.max_dbs(4) // Metadata and bodies (possibly v1 of both as well)
|
||||
.open(db_path)?
|
||||
};
|
||||
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
|
||||
let mut txn = db_env.write_txn()?;
|
||||
let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
|
||||
let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
|
||||
|
||||
// Remove edit workflow prompt, as we decided to opt into it using
|
||||
// a slash command instead.
|
||||
metadata.delete(&mut txn, &PromptId::EditWorkflow).ok();
|
||||
bodies.delete(&mut txn, &PromptId::EditWorkflow).ok();
|
||||
// Remove edit workflow prompt, as we decided to opt into it using
|
||||
// a slash command instead.
|
||||
metadata.delete(&mut txn, &PromptId::EditWorkflow).ok();
|
||||
bodies.delete(&mut txn, &PromptId::EditWorkflow).ok();
|
||||
|
||||
txn.commit()?;
|
||||
txn.commit()?;
|
||||
|
||||
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
|
||||
Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
|
||||
|
||||
let txn = db_env.read_txn()?;
|
||||
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
||||
txn.commit()?;
|
||||
let txn = db_env.read_txn()?;
|
||||
let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(PromptStore {
|
||||
executor,
|
||||
env: db_env,
|
||||
metadata_cache: RwLock::new(metadata_cache),
|
||||
metadata,
|
||||
bodies,
|
||||
})
|
||||
}
|
||||
Ok(PromptStore {
|
||||
env: db_env,
|
||||
metadata_cache: RwLock::new(metadata_cache),
|
||||
metadata,
|
||||
bodies,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -237,10 +243,10 @@ impl PromptStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(&self, id: PromptId) -> Task<Result<String>> {
|
||||
pub fn load(&self, id: PromptId, cx: &App) -> Task<Result<String>> {
|
||||
let env = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
self.executor.spawn(async move {
|
||||
cx.background_spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let mut prompt = bodies
|
||||
.get(&txn, &id)?
|
||||
@@ -262,21 +268,27 @@ impl PromptStore {
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
pub fn delete(&self, id: PromptId) -> Task<Result<()>> {
|
||||
pub fn delete(&self, id: PromptId, cx: &Context<Self>) -> Task<Result<()>> {
|
||||
self.metadata_cache.write().remove(id);
|
||||
|
||||
let db_connection = self.env.clone();
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.delete(&mut txn, &id)?;
|
||||
bodies.delete(&mut txn, &id)?;
|
||||
|
||||
txn.commit()?;
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -302,10 +314,10 @@ impl PromptStore {
|
||||
Some(metadata.id)
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String) -> Task<Vec<PromptMetadata>> {
|
||||
pub fn search(&self, query: String, cx: &App) -> Task<Vec<PromptMetadata>> {
|
||||
let cached_metadata = self.metadata_cache.read().metadata.clone();
|
||||
let executor = self.executor.clone();
|
||||
self.executor.spawn(async move {
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_spawn(async move {
|
||||
let mut matches = if query.is_empty() {
|
||||
cached_metadata
|
||||
} else {
|
||||
@@ -341,6 +353,7 @@ impl PromptStore {
|
||||
title: Option<SharedString>,
|
||||
default: bool,
|
||||
body: Rope,
|
||||
cx: &Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if id.is_built_in() {
|
||||
return Task::ready(Err(anyhow!("built-in prompts cannot be saved")));
|
||||
@@ -358,7 +371,7 @@ impl PromptStore {
|
||||
let bodies = self.bodies;
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
@@ -366,7 +379,13 @@ impl PromptStore {
|
||||
|
||||
txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -375,6 +394,7 @@ impl PromptStore {
|
||||
id: PromptId,
|
||||
mut title: Option<SharedString>,
|
||||
default: bool,
|
||||
cx: &Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let mut cache = self.metadata_cache.write();
|
||||
|
||||
@@ -397,19 +417,23 @@ impl PromptStore {
|
||||
let db_connection = self.env.clone();
|
||||
let metadata = self.metadata;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let task = cx.background_spawn(async move {
|
||||
let mut txn = db_connection.write_txn()?;
|
||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
||||
txn.commit()?;
|
||||
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
task.await?;
|
||||
this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a shared future to a prompt store so it can be assigned as a context global.
|
||||
pub struct GlobalPromptStore(
|
||||
Shared<BoxFuture<'static, Result<Arc<PromptStore>, Arc<anyhow::Error>>>>,
|
||||
);
|
||||
pub struct GlobalPromptStore(Shared<Task<Result<Entity<PromptStore>, Arc<anyhow::Error>>>>);
|
||||
|
||||
impl Global for GlobalPromptStore {}
|
||||
|
||||
@@ -15,24 +15,34 @@ use std::{
|
||||
};
|
||||
use text::LineEnding;
|
||||
use util::{ResultExt, get_system_shell};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ProjectContext {
|
||||
pub worktrees: Vec<WorktreeContext>,
|
||||
/// Whether any worktree has a rules_file. Provided as a field because handlebars can't do this.
|
||||
pub has_rules: bool,
|
||||
pub default_user_rules: Vec<DefaultUserRulesContext>,
|
||||
/// `!default_user_rules.is_empty()` - provided as a field because handlebars can't do this.
|
||||
pub has_default_user_rules: bool,
|
||||
pub os: String,
|
||||
pub arch: String,
|
||||
pub shell: String,
|
||||
}
|
||||
|
||||
impl ProjectContext {
|
||||
pub fn new(worktrees: Vec<WorktreeContext>) -> Self {
|
||||
pub fn new(
|
||||
worktrees: Vec<WorktreeContext>,
|
||||
default_user_rules: Vec<DefaultUserRulesContext>,
|
||||
) -> Self {
|
||||
let has_rules = worktrees
|
||||
.iter()
|
||||
.any(|worktree| worktree.rules_file.is_some());
|
||||
Self {
|
||||
worktrees,
|
||||
has_rules,
|
||||
has_default_user_rules: !default_user_rules.is_empty(),
|
||||
default_user_rules,
|
||||
os: std::env::consts::OS.to_string(),
|
||||
arch: std::env::consts::ARCH.to_string(),
|
||||
shell: get_system_shell(),
|
||||
@@ -40,6 +50,13 @@ impl ProjectContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DefaultUserRulesContext {
|
||||
pub uuid: Uuid,
|
||||
pub title: Option<String>,
|
||||
pub contents: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct WorktreeContext {
|
||||
pub root_name: String,
|
||||
@@ -377,3 +394,31 @@ impl PromptBuilder {
|
||||
self.handlebars.lock().render("suggest_edits", &())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_assistant_system_prompt_renders() {
|
||||
let worktrees = vec![WorktreeContext {
|
||||
root_name: "path".into(),
|
||||
abs_path: Path::new("/some/path").into(),
|
||||
rules_file: Some(RulesFileContext {
|
||||
path_in_worktree: Path::new(".rules").into(),
|
||||
abs_path: Path::new("/some/path/.rules").into(),
|
||||
text: "".into(),
|
||||
}),
|
||||
}];
|
||||
let default_user_rules = vec![DefaultUserRulesContext {
|
||||
uuid: Uuid::nil(),
|
||||
title: Some("Rules title".into()),
|
||||
contents: "Rules contents".into(),
|
||||
}];
|
||||
let project_context = ProjectContext::new(worktrees, default_user_rules);
|
||||
PromptBuilder::new(None)
|
||||
.unwrap()
|
||||
.generate_assistant_system_prompt(&project_context)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ message GetPrivateUserInfoResponse {
|
||||
enum Plan {
|
||||
Free = 0;
|
||||
ZedPro = 1;
|
||||
ZedProTrial = 2;
|
||||
}
|
||||
|
||||
message UpdateUserPlan {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
|
||||
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
|
||||
|
||||
pub const MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME: &str = "x-zed-llm-max-monthly-spend-reached";
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LanguageModelProvider {
|
||||
Anthropic,
|
||||
OpenAi,
|
||||
Google,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LanguageModel {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListModelsResponse {
|
||||
pub models: Vec<LanguageModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PerformCompletionParams {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub model: String,
|
||||
pub provider_request: Box<serde_json::value::RawValue>,
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
pub mod auth;
|
||||
mod conn;
|
||||
mod extension;
|
||||
mod llm;
|
||||
mod message_stream;
|
||||
mod notification;
|
||||
mod peer;
|
||||
|
||||
pub use conn::Connection;
|
||||
pub use extension::*;
|
||||
pub use llm::*;
|
||||
pub use notification::*;
|
||||
pub use peer::*;
|
||||
pub use proto;
|
||||
|
||||
@@ -557,6 +557,8 @@ impl SummaryIndex {
|
||||
);
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![prompt.into()],
|
||||
|
||||
@@ -23,3 +23,8 @@ snippet.workspace = true
|
||||
util.workspace = true
|
||||
schemars.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -222,15 +222,15 @@ impl SnippetProvider {
|
||||
.lookup_snippets::<false>(language, cx),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(registry) = SnippetRegistry::try_global(cx) else {
|
||||
return user_snippets;
|
||||
};
|
||||
|
||||
let registry_snippets = registry.get_snippets(language);
|
||||
user_snippets.extend(registry_snippets);
|
||||
}
|
||||
|
||||
let Some(registry) = SnippetRegistry::try_global(cx) else {
|
||||
return user_snippets;
|
||||
};
|
||||
|
||||
let registry_snippets = registry.get_snippets(language);
|
||||
user_snippets.extend(registry_snippets);
|
||||
|
||||
user_snippets
|
||||
}
|
||||
|
||||
@@ -244,3 +244,38 @@ impl SnippetProvider {
|
||||
requested_snippets
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui;
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_lookup_snippets_dup_registry_snippets(cx: &mut TestAppContext) {
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
cx.update(|cx| {
|
||||
SnippetRegistry::init_global(cx);
|
||||
SnippetRegistry::global(cx)
|
||||
.register_snippets(
|
||||
"ruby".as_ref(),
|
||||
indoc! {r#"
|
||||
{
|
||||
"Log to console": {
|
||||
"prefix": "log",
|
||||
"body": ["console.info(\"Hello, ${1:World}!\")", "$0"],
|
||||
"description": "Logs to console"
|
||||
}
|
||||
}
|
||||
"#},
|
||||
)
|
||||
.unwrap();
|
||||
let provider = SnippetProvider::new(fs.clone(), Default::default(), cx);
|
||||
cx.update_entity(&provider, |provider, cx| {
|
||||
assert_eq!(1, provider.snippets_for(Some("ruby".to_owned()), cx).len());
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use ui::{
|
||||
IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
|
||||
|
||||
pub use onboarding_banner::restore_banner;
|
||||
@@ -210,7 +210,6 @@ impl Render for TitleBar {
|
||||
.pr_1()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.children(self.render_call_controls(window, cx))
|
||||
.child(self.render_bottom_dock_layout_menu(cx))
|
||||
.map(|el| {
|
||||
let status = self.client.status();
|
||||
let status = &*status.borrow();
|
||||
@@ -623,101 +622,6 @@ impl TitleBar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_bottom_dock_layout_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let workspace = self.workspace.upgrade().unwrap();
|
||||
let current_layout = workspace.update(cx, |workspace, _cx| workspace.bottom_dock_layout());
|
||||
|
||||
PopoverMenu::new("layout-menu")
|
||||
.trigger(
|
||||
IconButton::new("toggle_layout", IconName::Layout)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Toggle Layout Menu")),
|
||||
)
|
||||
.anchor(gpui::Corner::TopRight)
|
||||
.menu(move |window, cx| {
|
||||
ContextMenu::build(window, cx, {
|
||||
let workspace = workspace.clone();
|
||||
move |menu, _, _| {
|
||||
menu.label("Bottom Dock")
|
||||
.separator()
|
||||
.toggleable_entry(
|
||||
"Contained",
|
||||
current_layout == BottomDockLayout::Contained,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::Contained,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Full",
|
||||
current_layout == BottomDockLayout::Full,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::Full,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Left Aligned",
|
||||
current_layout == BottomDockLayout::LeftAligned,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::LeftAligned,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.toggleable_entry(
|
||||
"Right Aligned",
|
||||
current_layout == BottomDockLayout::RightAligned,
|
||||
ui::IconPosition::End,
|
||||
None,
|
||||
{
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_bottom_dock_layout(
|
||||
BottomDockLayout::RightAligned,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
|
||||
let client = self.client.clone();
|
||||
Button::new("sign_in", "Sign in")
|
||||
@@ -751,6 +655,7 @@ impl TitleBar {
|
||||
None => "",
|
||||
Some(proto::Plan::Free) => "Free",
|
||||
Some(proto::Plan::ZedPro) => "Pro",
|
||||
Some(proto::Plan::ZedProTrial) => "Pro (Trial)",
|
||||
}
|
||||
),
|
||||
zed_actions::OpenAccountSettings.boxed_clone(),
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::prelude::*;
|
||||
/// A progress bar is a horizontal bar that communicates the status of a process.
|
||||
///
|
||||
/// A progress bar should not be used to represent indeterminate progress.
|
||||
#[derive(RegisterComponent, Documented)]
|
||||
#[derive(IntoElement, RegisterComponent, Documented)]
|
||||
pub struct ProgressBar {
|
||||
id: ElementId,
|
||||
value: f32,
|
||||
@@ -17,13 +17,7 @@ pub struct ProgressBar {
|
||||
}
|
||||
|
||||
impl ProgressBar {
|
||||
/// Create a new progress bar with the given value and maximum value.
|
||||
pub fn new(
|
||||
id: impl Into<ElementId>,
|
||||
value: f32,
|
||||
max_value: f32,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub fn new(id: impl Into<ElementId>, value: f32, max_value: f32, cx: &App) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
value,
|
||||
@@ -33,33 +27,33 @@ impl ProgressBar {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current value of the progress bar.
|
||||
pub fn value(&mut self, value: f32) -> &mut Self {
|
||||
/// Sets the current value of the progress bar.
|
||||
pub fn value(mut self, value: f32) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum value of the progress bar.
|
||||
pub fn max_value(&mut self, max_value: f32) -> &mut Self {
|
||||
/// Sets the maximum value of the progress bar.
|
||||
pub fn max_value(mut self, max_value: f32) -> Self {
|
||||
self.max_value = max_value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the background color of the progress bar.
|
||||
pub fn bg_color(&mut self, color: Hsla) -> &mut Self {
|
||||
/// Sets the background color of the progress bar.
|
||||
pub fn bg_color(mut self, color: Hsla) -> Self {
|
||||
self.bg_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the foreground color of the progress bar.
|
||||
pub fn fg_color(&mut self, color: Hsla) -> &mut Self {
|
||||
/// Sets the foreground color of the progress bar.
|
||||
pub fn fg_color(mut self, color: Hsla) -> Self {
|
||||
self.fg_color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProgressBar {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
impl RenderOnce for ProgressBar {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let fill_width = (self.value / self.max_value).clamp(0.02, 1.0);
|
||||
|
||||
div()
|
||||
@@ -98,11 +92,6 @@ impl Component for ProgressBar {
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let max_value = 180.0;
|
||||
|
||||
let empty_progress_bar = cx.new(|cx| ProgressBar::new("empty", 0.0, max_value, cx));
|
||||
let partial_progress_bar =
|
||||
cx.new(|cx| ProgressBar::new("partial", max_value * 0.35, max_value, cx));
|
||||
let filled_progress_bar = cx.new(|cx| ProgressBar::new("filled", max_value, max_value, cx));
|
||||
|
||||
Some(
|
||||
div()
|
||||
.flex()
|
||||
@@ -123,7 +112,7 @@ impl Component for ProgressBar {
|
||||
.child(Label::new("0%"))
|
||||
.child(Label::new("Empty")),
|
||||
)
|
||||
.child(empty_progress_bar.clone()),
|
||||
.child(ProgressBar::new("empty", 0.0, max_value, cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -137,7 +126,7 @@ impl Component for ProgressBar {
|
||||
.child(Label::new("38%"))
|
||||
.child(Label::new("Partial")),
|
||||
)
|
||||
.child(partial_progress_bar.clone()),
|
||||
.child(ProgressBar::new("partial", max_value * 0.35, max_value, cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -151,7 +140,7 @@ impl Component for ProgressBar {
|
||||
.child(Label::new("100%"))
|
||||
.child(Label::new("Complete")),
|
||||
)
|
||||
.child(filled_progress_bar.clone()),
|
||||
.child(ProgressBar::new("filled", max_value, max_value, cx)),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
|
||||
@@ -73,11 +73,11 @@ impl Tab {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn content_height(cx: &mut App) -> Pixels {
|
||||
pub fn content_height(cx: &App) -> Pixels {
|
||||
DynamicSpacing::Base32.px(cx) - px(1.)
|
||||
}
|
||||
|
||||
pub fn container_height(cx: &mut App) -> Pixels {
|
||||
pub fn container_height(cx: &App) -> Pixels {
|
||||
DynamicSpacing::Base32.px(cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,11 @@ impl Render for Tooltip {
|
||||
}),
|
||||
)
|
||||
.when_some(self.meta.clone(), |this, meta| {
|
||||
this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
|
||||
this.child(
|
||||
div()
|
||||
.max_w_72()
|
||||
.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
20
crates/web_search/Cargo.toml
Normal file
20
crates/web_search/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "web_search"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/web_search.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
1
crates/web_search/LICENSE-GPL
Symbolic link
1
crates/web_search/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user