Compare commits
226 Commits
data-break
...
project-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8001cb9037 | ||
|
|
18144e7d63 | ||
|
|
f2bb495bff | ||
|
|
1106ba81ff | ||
|
|
3a9fe61516 | ||
|
|
28cca2527c | ||
|
|
3fde499eee | ||
|
|
f176715f1e | ||
|
|
4a302a28b7 | ||
|
|
3154b530a0 | ||
|
|
959d5f52a4 | ||
|
|
2771623bac | ||
|
|
920487706d | ||
|
|
3d5035d8a6 | ||
|
|
d047801295 | ||
|
|
e7d1d731f4 | ||
|
|
149db5cad4 | ||
|
|
992d791852 | ||
|
|
18bf07822e | ||
|
|
3ba619db5c | ||
|
|
3559464840 | ||
|
|
32a03ee58a | ||
|
|
912ce5dc1a | ||
|
|
97e8f499fd | ||
|
|
736d5e6da1 | ||
|
|
68023fdc44 | ||
|
|
253ef7ef44 | ||
|
|
687cc5d661 | ||
|
|
290d249e38 | ||
|
|
4922a000aa | ||
|
|
96c9824997 | ||
|
|
a83151730b | ||
|
|
99335128eb | ||
|
|
e8457656c6 | ||
|
|
b037c913e5 | ||
|
|
aca7f54773 | ||
|
|
10a3ad078c | ||
|
|
c4bbdd03c5 | ||
|
|
27a47233c9 | ||
|
|
0b8c14f0d6 | ||
|
|
800f40524b | ||
|
|
1f4a2d50a0 | ||
|
|
c706acbfcb | ||
|
|
3f19ae1689 | ||
|
|
4744c4ff7e | ||
|
|
aa168c696b | ||
|
|
ce2918ef46 | ||
|
|
3cfaadd2af | ||
|
|
b69219e9e0 | ||
|
|
0ae3518b1c | ||
|
|
73964353a4 | ||
|
|
bf6e7cb6ee | ||
|
|
36c4f6082c | ||
|
|
3d032bcf2c | ||
|
|
3e28fa2cc4 | ||
|
|
c42442974d | ||
|
|
f23b972203 | ||
|
|
07d9cd7e88 | ||
|
|
a48238701d | ||
|
|
d17d747c62 | ||
|
|
bc08df2dfd | ||
|
|
0f4b734d91 | ||
|
|
6df29d3279 | ||
|
|
647cca8c8d | ||
|
|
1f81674927 | ||
|
|
e1a8a31fa4 | ||
|
|
0be8bf1b12 | ||
|
|
3e2ac3e7bc | ||
|
|
646f65511c | ||
|
|
b2904e5d9f | ||
|
|
45e7cd1638 | ||
|
|
889bc13b7d | ||
|
|
da3383b10e | ||
|
|
9c4e61eae1 | ||
|
|
b413605f6b | ||
|
|
961bfadad9 | ||
|
|
2eed94ff23 | ||
|
|
7a54dd7190 | ||
|
|
8ac4cbcbb9 | ||
|
|
d82bf132ca | ||
|
|
0f58d4f533 | ||
|
|
d340670fd4 | ||
|
|
ad42a8ad9b | ||
|
|
2846c55cde | ||
|
|
5dcd0d37a6 | ||
|
|
a70daa4310 | ||
|
|
b158ee60ca | ||
|
|
9b7b4a9d2d | ||
|
|
c1d6dfd832 | ||
|
|
57d7bc23ae | ||
|
|
7bc0dd1bf6 | ||
|
|
dc83f1ad38 | ||
|
|
8e0f70f3c7 | ||
|
|
6262a4638b | ||
|
|
8321e65561 | ||
|
|
4110928314 | ||
|
|
a1b53e91e7 | ||
|
|
4a252515b1 | ||
|
|
d26c477d86 | ||
|
|
16f625bd07 | ||
|
|
9bc4697a33 | ||
|
|
e7290df02b | ||
|
|
8f25251faf | ||
|
|
1567967309 | ||
|
|
2d05537b59 | ||
|
|
95b963c87b | ||
|
|
5d1695ed1d | ||
|
|
192097f58f | ||
|
|
92059803fb | ||
|
|
5880271b11 | ||
|
|
4ff07bf789 | ||
|
|
b59967bbdf | ||
|
|
133d3f052f | ||
|
|
0934cb58c4 | ||
|
|
bda33ec436 | ||
|
|
64ef3ab09d | ||
|
|
ac5a2b2122 | ||
|
|
ada8b0f822 | ||
|
|
76871056f5 | ||
|
|
f859b328f0 | ||
|
|
e63df58adc | ||
|
|
0ad4656489 | ||
|
|
992831ceb6 | ||
|
|
7257053df4 | ||
|
|
feb1d37798 | ||
|
|
12037dc2c6 | ||
|
|
ab31eb5d51 | ||
|
|
7d67bd480b | ||
|
|
a24fc5a1a5 | ||
|
|
0079771e34 | ||
|
|
d1db6d6782 | ||
|
|
5509e0141a | ||
|
|
8be5ed22f6 | ||
|
|
5343f1cdaf | ||
|
|
8075c2458f | ||
|
|
d0276e6666 | ||
|
|
c729842804 | ||
|
|
715e23a491 | ||
|
|
63f0fda350 | ||
|
|
7984f0f11c | ||
|
|
37ebb47238 | ||
|
|
8546dc101d | ||
|
|
33912011b7 | ||
|
|
dce824f095 | ||
|
|
a1bef28da3 | ||
|
|
8a212be0b1 | ||
|
|
9bbb1e5476 | ||
|
|
50ad71a630 | ||
|
|
76c46c5bab | ||
|
|
8f0bacddd8 | ||
|
|
051483200d | ||
|
|
27cafe5567 | ||
|
|
ddc102c7e0 | ||
|
|
d517a212dc | ||
|
|
dc64ec9cc8 | ||
|
|
d50905e000 | ||
|
|
0729d24d77 | ||
|
|
d51aa2ffb0 | ||
|
|
d40b49ceb9 | ||
|
|
edf712d45b | ||
|
|
627ae7af6f | ||
|
|
17aecfde6f | ||
|
|
01a2c8eb01 | ||
|
|
fc269dfaf9 | ||
|
|
e1e8c1786e | ||
|
|
c8a9a74e6a | ||
|
|
f6d58f76e4 | ||
|
|
e6c64ebf7e | ||
|
|
12c58d01bb | ||
|
|
75689c1c88 | ||
|
|
ca6be249dc | ||
|
|
9b44bacc28 | ||
|
|
9b40770e9f | ||
|
|
5b2adfbb50 | ||
|
|
078b241223 | ||
|
|
e42406f9d5 | ||
|
|
4ee20dda23 | ||
|
|
74dd32d52c | ||
|
|
342acdd080 | ||
|
|
9f8776d1af | ||
|
|
548a8d75e6 | ||
|
|
73f77a7fbb | ||
|
|
4970fe2d56 | ||
|
|
7fe6188f8e | ||
|
|
8add90d7cb | ||
|
|
141a6c3915 | ||
|
|
b4254a33e0 | ||
|
|
f86977e2a7 | ||
|
|
8ecf553279 | ||
|
|
e171d16ae3 | ||
|
|
35da9c0cdc | ||
|
|
8b3eb98d86 | ||
|
|
d912b0dd36 | ||
|
|
044508ef77 | ||
|
|
d63658cee2 | ||
|
|
790b56f52c | ||
|
|
43142000a6 | ||
|
|
3e400a4969 | ||
|
|
e11e7df724 | ||
|
|
bbd1e628f0 | ||
|
|
01b400ea29 | ||
|
|
9445005bff | ||
|
|
5c0adde7bb | ||
|
|
4a5c492188 | ||
|
|
4da987dad4 | ||
|
|
be657aefa3 | ||
|
|
d0ae604eda | ||
|
|
08bb17a7eb | ||
|
|
55c1f9d26c | ||
|
|
28f0ba3381 | ||
|
|
b5dc09c0ca | ||
|
|
e90411efa2 | ||
|
|
fcadcbb510 | ||
|
|
94ed0b7767 | ||
|
|
8a307e7b89 | ||
|
|
c8fb95cd1b | ||
|
|
55d934a3be | ||
|
|
a916bbf00c | ||
|
|
da47013e56 | ||
|
|
82ce187bfc | ||
|
|
e843790490 | ||
|
|
83f3fbfef5 | ||
|
|
60a7455f12 | ||
|
|
4315b2fc8a | ||
|
|
68d453da52 | ||
|
|
20eab9038f |
@@ -14,10 +14,10 @@ linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
|
||||
[target.aarch64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-Objc -all_load"]
|
||||
rustflags = ["-C", "link-args=-all_load"]
|
||||
|
||||
[target.x86_64-apple-darwin]
|
||||
rustflags = ["-C", "link-args=-Objc -all_load"]
|
||||
rustflags = ["-C", "link-args=-all_load"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = [
|
||||
|
||||
43
.config/hakari.toml
Normal file
43
.config/hakari.toml
Normal file
@@ -0,0 +1,43 @@
|
||||
# This file contains settings for `cargo hakari`.
|
||||
# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options.
|
||||
|
||||
hakari-package = "workspace-hack"
|
||||
|
||||
resolver = "2"
|
||||
dep-format-version = "4"
|
||||
workspace-hack-line-style = "workspace-dotted"
|
||||
|
||||
# this should be the same list as "targets" in ../rust-toolchain.toml
|
||||
platforms = [
|
||||
"x86_64-apple-darwin",
|
||||
"aarch64-apple-darwin",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-musl", # remote server
|
||||
]
|
||||
|
||||
[traversal-excludes]
|
||||
workspace-members = [
|
||||
"remote_server",
|
||||
]
|
||||
third-party = [
|
||||
{ name = "reqwest", version = "0.11.27" },
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
workspace-members = [
|
||||
"zed_extension_api",
|
||||
|
||||
# exclude all extensions
|
||||
"zed_emmet",
|
||||
"zed_glsl",
|
||||
"zed_html",
|
||||
"perplexity",
|
||||
"zed_proto",
|
||||
"zed_ruff",
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
"zed_toml",
|
||||
]
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -110,6 +110,37 @@ jobs:
|
||||
input: "crates/proto/proto/"
|
||||
against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/"
|
||||
|
||||
workspace_hack:
|
||||
timeout-minutes: 60
|
||||
name: Check workspace-hack crate
|
||||
needs: [job_spec]
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- name: Install cargo-hakari
|
||||
uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hakari@0.9.35
|
||||
|
||||
- name: Check workspace-hack Cargo.toml is up-to-date
|
||||
run: |
|
||||
cargo hakari generate --diff || {
|
||||
echo "To fix, run script/update-workspace-hack";
|
||||
false
|
||||
}
|
||||
- name: Check all crates depend on workspace-hack
|
||||
run: |
|
||||
cargo hakari manage-deps --dry-run || {
|
||||
echo "To fix, run script/update-workspace-hack"
|
||||
false
|
||||
}
|
||||
|
||||
style:
|
||||
timeout-minutes: 60
|
||||
name: Check formatting and spelling
|
||||
@@ -209,7 +240,6 @@ jobs:
|
||||
cargo check -p workspace
|
||||
cargo build -p remote_server
|
||||
cargo check -p gpui --examples
|
||||
script/check-rust-livekit-macos
|
||||
|
||||
# Since the macOS runners are stateful, so we need to remove the config file to prevent potential bug.
|
||||
- name: Clean CI config file
|
||||
@@ -433,6 +463,7 @@ jobs:
|
||||
- job_spec
|
||||
- style
|
||||
- migration_checks
|
||||
- workspace_hack
|
||||
- linux_tests
|
||||
- build_remote_server
|
||||
- macos_tests
|
||||
|
||||
@@ -59,7 +59,9 @@ jobs:
|
||||
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
echo \"${{ toJSON(github.event.release.body) }}\" > release_body.txt
|
||||
cat << 'EOF' > release_body.txt
|
||||
${{ github.event.release.body }}
|
||||
EOF
|
||||
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
|
||||
> release_data.json
|
||||
curl -X POST "https://zed.dev/api/send_release_notes_email" \
|
||||
|
||||
3
.github/workflows/release_nightly.yml
vendored
3
.github/workflows/release_nightly.yml
vendored
@@ -184,9 +184,6 @@ jobs:
|
||||
- os: arm Mac
|
||||
runner: [macOS, ARM64, test]
|
||||
install_nix: false
|
||||
- os: arm Linux
|
||||
runner: buildjet-16vcpu-ubuntu-2204-arm
|
||||
install_nix: true
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ${{ matrix.system.runner }}
|
||||
needs: tests
|
||||
|
||||
2518
Cargo.lock
generated
2518
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
41
Cargo.toml
41
Cargo.toml
@@ -2,11 +2,11 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/assistant_context_editor",
|
||||
"crates/assistant_eval",
|
||||
"crates/assistant_settings",
|
||||
@@ -87,7 +87,6 @@ members = [
|
||||
"crates/languages",
|
||||
"crates/livekit_api",
|
||||
"crates/livekit_client",
|
||||
"crates/livekit_client_macos",
|
||||
"crates/lmstudio",
|
||||
"crates/lsp",
|
||||
"crates/markdown",
|
||||
@@ -193,13 +192,14 @@ members = [
|
||||
# Tooling
|
||||
#
|
||||
|
||||
"tooling/workspace-hack",
|
||||
"tooling/xtask",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
|
||||
[workspace.package]
|
||||
publish = false
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
||||
@@ -208,12 +208,12 @@ edition = "2021"
|
||||
#
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent = { path = "crates/agent" }
|
||||
ai = { path = "crates/ai" }
|
||||
anthropic = { path = "crates/anthropic" }
|
||||
askpass = { path = "crates/askpass" }
|
||||
assets = { path = "crates/assets" }
|
||||
assistant = { path = "crates/assistant" }
|
||||
assistant2 = { path = "crates/assistant2" }
|
||||
assistant_context_editor = { path = "crates/assistant_context_editor" }
|
||||
assistant_eval = { path = "crates/assistant_eval" }
|
||||
assistant_settings = { path = "crates/assistant_settings" }
|
||||
@@ -292,7 +292,6 @@ language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
livekit_api = { path = "crates/livekit_api" }
|
||||
livekit_client = { path = "crates/livekit_client" }
|
||||
livekit_client_macos = { path = "crates/livekit_client_macos" }
|
||||
lmstudio = { path = "crates/lmstudio" }
|
||||
lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
@@ -400,11 +399,11 @@ async-trait = "0.1"
|
||||
async-tungstenite = "0.28"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
aws-config = { version = "1.5.16", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }
|
||||
aws-sdk-bedrockruntime = { version = "1.73.0", features = ["behavior-version-latest"] }
|
||||
aws-smithy-runtime-api = { version = "1.7.3", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.2.13", features = ["http-body-1-x"] }
|
||||
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { version = "1.2.2", features = ["hardcoded-credentials"] }
|
||||
aws-sdk-bedrockruntime = { version = "1.80.0", features = ["behavior-version-latest"] }
|
||||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
|
||||
@@ -420,8 +419,9 @@ circular-buffer = "1.0"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
cocoa = "0.26"
|
||||
cocoa-foundation = "0.2.0"
|
||||
core-video = { version = "0.4.3", features = ["metal"] }
|
||||
convert_case = "0.8.0"
|
||||
core-foundation = "0.9.3"
|
||||
core-foundation = "0.10.0"
|
||||
core-foundation-sys = "0.8.6"
|
||||
ctor = "0.4.0"
|
||||
dashmap = "6.0"
|
||||
@@ -459,11 +459,6 @@ libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
linkme = "0.3.31"
|
||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "811ceae29fabee455f110c56cd66b3f49a7e5003", features = [
|
||||
"dispatcher",
|
||||
"services-dispatcher",
|
||||
"rustls-tls-native-roots",
|
||||
], default-features = false }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
@@ -552,6 +547,7 @@ time = { version = "0.3", features = [
|
||||
tiny_http = "0.8"
|
||||
toml = "0.8"
|
||||
tokio = { version = "1" }
|
||||
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"]}
|
||||
tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.3", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.23"
|
||||
@@ -573,7 +569,7 @@ tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-ma
|
||||
tree-sitter-python = "0.23"
|
||||
tree-sitter-regex = "0.24"
|
||||
tree-sitter-ruby = "0.23"
|
||||
tree-sitter-rust = "0.23"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = "0.23"
|
||||
tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" }
|
||||
unicase = "2.6"
|
||||
@@ -583,6 +579,7 @@ unicode-script = "0.5.7"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.3"
|
||||
wasmparser = "0.221"
|
||||
wasm-encoder = "0.221"
|
||||
wasmtime = { version = "29", default-features = false, features = [
|
||||
@@ -595,9 +592,10 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.4"
|
||||
zstd = "0.11"
|
||||
metal = "0.31"
|
||||
metal = "0.29"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
git = "https://github.com/zed-industries/async-stripe"
|
||||
@@ -662,6 +660,11 @@ features = [
|
||||
[patch.crates-io]
|
||||
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
|
||||
real-async-tls = { git = "https://github.com/zed-industries/async-tls", rev = "1e759a4b5e370f87dc15e40756ac4f8815b61d9d", package = "async-tls" }
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
|
||||
# Makes the workspace hack crate refer to the local one, but only when you're building locally
|
||||
workspace-hack = { path = "tooling/workspace-hack" }
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
@@ -775,4 +778,4 @@ let_underscore_future = "allow"
|
||||
too_many_arguments = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme"]
|
||||
ignored = ["bindgen", "cbindgen", "prost_build", "serde", "component", "linkme", "workspace-hack"]
|
||||
|
||||
1
assets/icons/check_double.svg
Normal file
1
assets/icons/check_double.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-check-icon lucide-check-check"><path d="M18 6 7 17l-5-5"/><path d="m22 10-7.5 7.5L13 16"/></svg>
|
||||
|
After Width: | Height: | Size: 305 B |
1
assets/icons/debug_disabled_breakpoint.svg
Normal file
1
assets/icons/debug_disabled_breakpoint.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>
|
||||
|
After Width: | Height: | Size: 249 B |
1
assets/icons/debug_disabled_log_breakpoint.svg
Normal file
1
assets/icons/debug_disabled_log_breakpoint.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
|
||||
|
After Width: | Height: | Size: 267 B |
1
assets/icons/forward_arrow.svg
Normal file
1
assets/icons/forward_arrow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-forward-icon lucide-forward"><polyline points="15 17 20 12 15 7"/><path d="M4 18v-2a4 4 0 0 1 4-4h12"/></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-more"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M8 12h.01"/><path d="M12 12h.01"/><path d="M16 12h.01"/></svg>
|
||||
|
Before Width: | Height: | Size: 337 B |
1
assets/icons/user_round_pen.svg
Normal file
1
assets/icons/user_round_pen.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-pen-icon lucide-user-round-pen"><path d="M2 21a8 8 0 0 1 10.821-7.487"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="8" r="5"/></svg>
|
||||
|
After Width: | Height: | Size: 461 B |
@@ -126,7 +126,6 @@
|
||||
// "alt-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-alt-space": "editor::ShowCharacterPalette",
|
||||
"ctrl-;": "editor::ToggleLineNumbers",
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-'": "editor::ToggleSelectedDiffHunks",
|
||||
"ctrl-\"": "editor::ExpandAllDiffHunks",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
@@ -138,6 +137,22 @@
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-r": "git::Restore",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"bindings": {
|
||||
"ctrl-y": "agent::Keep",
|
||||
"ctrl-k ctrl-r": "agent::Reject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
@@ -382,9 +397,6 @@
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||
"ctrl-alt-y": "git::ToggleStaged",
|
||||
"alt-y": "git::StageAndNext",
|
||||
"alt-shift-y": "git::UnstageAndNext",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
@@ -582,13 +594,6 @@
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProposedChangesEditor",
|
||||
"bindings": {
|
||||
"ctrl-shift-y": "editor::ApplyDiffHunk",
|
||||
"ctrl-alt-a": "editor::ApplyAllDiffHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
@@ -613,29 +618,34 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2",
|
||||
"context": "AgentPanel",
|
||||
"bindings": {
|
||||
"ctrl-n": "assistant2::NewThread",
|
||||
"new": "assistant2::NewThread",
|
||||
"ctrl-shift-h": "assistant2::OpenHistory",
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"new": "agent::NewThread",
|
||||
"ctrl-alt-n": "agent::NewPromptEditor",
|
||||
"ctrl-shift-h": "agent::OpenHistory",
|
||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "assistant::ToggleModelSelector",
|
||||
"ctrl-shift-a": "assistant2::ToggleContextPicker",
|
||||
"ctrl-e": "assistant2::ChatMode",
|
||||
"ctrl-alt-e": "assistant2::RemoveAllContext"
|
||||
"ctrl-shift-a": "agent::ToggleContextPicker",
|
||||
"ctrl-e": "agent::ChatMode",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2 && prompt_editor",
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-t": "assistant2::NewThread"
|
||||
"cmd-n": "agent::NewPromptEditor",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat"
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -647,21 +657,30 @@
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"bindings": {
|
||||
"up": "assistant2::FocusUp",
|
||||
"right": "assistant2::FocusRight",
|
||||
"left": "assistant2::FocusLeft",
|
||||
"down": "assistant2::FocusDown",
|
||||
"backspace": "assistant2::RemoveFocusedContext",
|
||||
"enter": "assistant2::AcceptSuggestedContext"
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"bindings": {
|
||||
"backspace": "assistant2::RemoveSelectedThread"
|
||||
"backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -669,7 +688,7 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist",
|
||||
"ctrl-alt-e": "assistant2::RemoveAllContext"
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -147,10 +147,6 @@
|
||||
"ctrl-shift-v": ["editor::MovePageUp", { "center_cursor": true }],
|
||||
"ctrl-cmd-space": "editor::ShowCharacterPalette",
|
||||
"cmd-;": "editor::ToggleLineNumbers",
|
||||
"cmd-alt-z": "git::Restore",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"cmd-y": "git::StageAndNext",
|
||||
"cmd-shift-y": "git::UnstageAndNext",
|
||||
"cmd-'": "editor::ToggleSelectedDiffHunks",
|
||||
"cmd-\"": "editor::ExpandAllDiffHunks",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
@@ -231,6 +227,24 @@
|
||||
"ctrl-alt-enter": "repl::RunInPlace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-z": "git::Restore",
|
||||
"cmd-alt-y": "git::ToggleStaged",
|
||||
"cmd-y": "git::StageAndNext",
|
||||
"cmd-shift-y": "git::UnstageAndNext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AgentDiff",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-y": "agent::Keep",
|
||||
"cmd-alt-z": "agent::Reject"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel",
|
||||
"use_key_equivalents": true,
|
||||
@@ -263,33 +277,35 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2",
|
||||
"context": "AgentPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewThread",
|
||||
"cmd-alt-p": "assistant2::NewPromptEditor",
|
||||
"cmd-shift-h": "assistant2::OpenHistory",
|
||||
"cmd-n": "agent::NewThread",
|
||||
"cmd-alt-n": "agent::NewPromptEditor",
|
||||
"cmd-shift-h": "agent::OpenHistory",
|
||||
"cmd-alt-c": "agent::OpenConfiguration",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker",
|
||||
"cmd-e": "assistant2::ChatMode",
|
||||
"cmd-alt-e": "assistant2::RemoveAllContext"
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-e": "agent::ChatMode",
|
||||
"cmd-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AssistantPanel2 && prompt_editor",
|
||||
"context": "AgentPanel && prompt_editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "assistant2::NewPromptEditor",
|
||||
"cmd-alt-t": "assistant2::NewThread"
|
||||
"cmd-n": "agent::NewPromptEditor",
|
||||
"cmd-alt-t": "agent::NewThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "assistant2::Chat",
|
||||
"cmd-g d": "git::Diff",
|
||||
"shift-escape": "git::ExpandCommitEditor"
|
||||
"enter": "agent::Chat",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -302,21 +318,30 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ContextStrip",
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "assistant2::FocusUp",
|
||||
"right": "assistant2::FocusRight",
|
||||
"left": "assistant2::FocusLeft",
|
||||
"down": "assistant2::FocusDown",
|
||||
"backspace": "assistant2::RemoveFocusedContext",
|
||||
"enter": "assistant2::AcceptSuggestedContext"
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory",
|
||||
"context": "ContextStrip",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"backspace": "assistant2::RemoveSelectedThread"
|
||||
"up": "agent::FocusUp",
|
||||
"right": "agent::FocusRight",
|
||||
"left": "agent::FocusLeft",
|
||||
"down": "agent::FocusDown",
|
||||
"backspace": "agent::RemoveFocusedContext",
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ThreadHistory > Editor",
|
||||
"bindings": {
|
||||
"shift-backspace": "agent::RemoveSelectedThread"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -688,21 +713,13 @@
|
||||
"ctrl-:": "editor::ToggleInlayHints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProposedChangesEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-y": "editor::ApplyDiffHunk",
|
||||
"cmd-shift-a": "editor::ApplyAllDiffHunks"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "assistant2::ToggleContextPicker",
|
||||
"cmd-shift-a": "agent::ToggleContextPicker",
|
||||
"cmd-alt-/": "assistant::ToggleModelSelector",
|
||||
"cmd-alt-e": "assistant2::RemoveAllContext",
|
||||
"cmd-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-[": "assistant::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "assistant::CycleNextInlineAssist"
|
||||
}
|
||||
|
||||
@@ -227,6 +227,8 @@
|
||||
"g u": "vim::PushLowercase",
|
||||
"g shift-u": "vim::PushUppercase",
|
||||
"g ~": "vim::PushOppositeCase",
|
||||
"g ?": "vim::PushRot13",
|
||||
// "g ?": "vim::PushRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
"g w": "vim::PushRewrap",
|
||||
"g q": "vim::PushRewrap",
|
||||
@@ -258,9 +260,10 @@
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
"shift-u": "vim::ConvertToUpperCase",
|
||||
"shift-o": "vim::OtherEnd",
|
||||
"o": "vim::OtherEnd",
|
||||
"o": "vim::OtherEndRowAware",
|
||||
"d": "vim::VisualDelete",
|
||||
"x": "vim::VisualDelete",
|
||||
"delete": "vim::VisualDelete",
|
||||
"shift-d": "vim::VisualDeleteLine",
|
||||
"shift-x": "vim::VisualDeleteLine",
|
||||
"y": "vim::VisualYank",
|
||||
@@ -297,6 +300,8 @@
|
||||
"g r": ["vim::Paste", { "preserve_clipboard": true }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"g q": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
// "g ?": "vim::ConvertToRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
@@ -476,6 +481,13 @@
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g?",
|
||||
"bindings": {
|
||||
"g ?": "vim::CurrentLine",
|
||||
"?": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gq",
|
||||
"bindings": {
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
You are an AI assistant integrated into a text editor. Your goal is to do one of the following two things:
|
||||
You are an AI assistant integrated into a code editor. You have the programming ability of an expert programmer who takes pride in writing high-quality code and is driven to the point of obsession about solving problems effectively. Your goal is to do one of the following two things:
|
||||
|
||||
1. Help users answer questions and perform tasks related to their codebase.
|
||||
2. Answer general-purpose questions unrelated to their particular codebase.
|
||||
|
||||
It will be up to you to decide which of these you are doing based on what the user has told you. When unclear, ask clarifying questions to understand the user's intent before proceeding.
|
||||
|
||||
You should only perform actions that modify the user’s system if explicitly requested by the user:
|
||||
You should only perform actions that modify the user's system if explicitly requested by the user:
|
||||
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user’s system without explicit instruction.
|
||||
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
|
||||
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
|
||||
Be concise and direct in your responses.
|
||||
Editing code:
|
||||
- Make sure to take previous edits into account.
|
||||
- The edits you perform might lead to errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- You may only attempt to fix these up to 3 times. If you have tried 3 times to fix them, and there are still problems remaining, you must not continue trying to fix them, and must instead tell the user that there are problems remaining - and ask if the user would like you to attempt to solve them further.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
- Prefer to move files over recreating them. The move can be followed by minor edits if required.
|
||||
- If you seem to be stuck, never go back and "simplify the implementation" by deleting the parts of the implementation you're stuck on and replacing them with comments. If you ever feel the urge to do this, instead immediately stop whatever you're doing (even if the code is in a broken state), report that you are stuck, explain what you're stuck on, and ask the user how to proceed.
|
||||
|
||||
Tool use:
|
||||
- Make sure to adhere to the tools schema.
|
||||
- Provide every required argument.
|
||||
- DO NOT use tools to access items that are already available in the context section.
|
||||
- Use only the tools that are currently available.
|
||||
- DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off.
|
||||
|
||||
Responding:
|
||||
- Be concise and direct in your responses.
|
||||
- Never apologize or thank the user.
|
||||
- Don't comment that you have just realized or understood something.
|
||||
- When you are going to make a tool call, tersely explain your reasoning for choosing to use that tool, with no flourishes or commentary beyond that information.
|
||||
For example, rather than saying "You're absolutely right! Thank you for providing that context. Now I understand that we're missing a dependency, and I need to add it:" say "I'll add that missing dependency:" instead.
|
||||
- Also, don't restate what a tool call is about to do (or just did).
|
||||
For example, don't say "Now I'm going to check diagnostics to see if there are any warnings or errors," followed by running a tool which checks diagnostics and reports warnings or errors; instead, just request the tool call without saying anything.
|
||||
- All tool results are provided to you automatically, so DO NOT thank the user when this happens.
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -115,6 +115,15 @@
|
||||
"confirm_quit": false,
|
||||
// Whether to restore last closed project when fresh Zed instance is opened.
|
||||
"restore_on_startup": "last_session",
|
||||
// Whether to attempt to restore previous file's state when opening it again.
|
||||
// The state is stored per pane.
|
||||
// When disabled, defaults are applied instead of the state restoration.
|
||||
//
|
||||
// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane.
|
||||
// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default.
|
||||
//
|
||||
// Default: true
|
||||
"restore_on_file_reopen": true,
|
||||
// Size of the drop target in the editor.
|
||||
"drop_target_size": 0.2,
|
||||
// Whether the window should be closed when using 'close active item' on a window with no tabs.
|
||||
@@ -155,8 +164,8 @@
|
||||
//
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
|
||||
"hide_mouse_while_typing": true,
|
||||
// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
"hide_mouse": "on_typing_and_movement",
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
// 1. Don't highlight the current line:
|
||||
@@ -624,45 +633,56 @@
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
},
|
||||
"default_profile": "code-writer",
|
||||
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
|
||||
"always_allow_tool_actions": false,
|
||||
"default_profile": "write",
|
||||
"profiles": {
|
||||
"read-only": {
|
||||
"name": "Read-only",
|
||||
"ask": {
|
||||
"name": "Ask",
|
||||
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
|
||||
// "enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
"list-directory": true,
|
||||
"list_directory": false,
|
||||
"now": true,
|
||||
"path-search": true,
|
||||
"read-file": true,
|
||||
"regex-search": true,
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"thinking": true
|
||||
}
|
||||
},
|
||||
"code-writer": {
|
||||
"name": "Code Writer",
|
||||
"write": {
|
||||
"name": "Write",
|
||||
"enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"bash": true,
|
||||
"batch-tool": true,
|
||||
"copy-path": true,
|
||||
"create-file": true,
|
||||
"delete-path": true,
|
||||
"batch_tool": true,
|
||||
"code_symbols": true,
|
||||
"copy_path": false,
|
||||
"create_file": true,
|
||||
"delete_path": false,
|
||||
"diagnostics": true,
|
||||
"find-replace-file": true,
|
||||
"edit-files": false,
|
||||
"find_replace_file": true,
|
||||
"fetch": true,
|
||||
"list-directory": true,
|
||||
"move-path": true,
|
||||
"list_directory": false,
|
||||
"move_path": false,
|
||||
"now": true,
|
||||
"path-search": true,
|
||||
"read-file": true,
|
||||
"regex-search": true,
|
||||
"path_search": true,
|
||||
"read_file": true,
|
||||
"regex_search": true,
|
||||
"symbol_info": true,
|
||||
"thinking": true
|
||||
}
|
||||
}
|
||||
},
|
||||
// Shows a notification when the agent needs confirmation before running an edit tool call or when that's concluded.
|
||||
"notify_when_agent_waiting": true
|
||||
// Where to show notifications when an agent has either completed
|
||||
// its response, or else needs confirmation before it can run a
|
||||
// tool action.
|
||||
// "primary_screen" - Show the notification only on your primary screen (default)
|
||||
// "all_screens" - Show these notifications on all screens
|
||||
// "never" - Never show these notifications
|
||||
"notify_when_agent_waiting": "primary_screen"
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -1466,11 +1486,6 @@
|
||||
"dev": {
|
||||
// "theme": "Andromeda"
|
||||
},
|
||||
// Task-related settings.
|
||||
"task": {
|
||||
// Whether to show task status indicator in the status bar. Default: true
|
||||
"show_status_indicator": true
|
||||
},
|
||||
// Whether to show full labels in line indicator or short ones
|
||||
//
|
||||
// Values:
|
||||
|
||||
@@ -25,6 +25,7 @@ smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -3,20 +3,20 @@ use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, percentage, Animation, AnimationExt as _, App, Context, CursorStyle, Entity,
|
||||
EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, Window,
|
||||
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
|
||||
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
|
||||
Styled, Transformation, Window, actions, percentage,
|
||||
};
|
||||
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
|
||||
use project::{
|
||||
EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
|
||||
ProjectEnvironmentEvent, WorktreeId,
|
||||
ProjectEnvironmentEvent,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
use ui::{prelude::*, ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use std::{cmp::Reverse, fmt::Write, path::Path, sync::Arc, time::Duration};
|
||||
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use util::truncate_and_trailoff;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
@@ -218,13 +218,14 @@ impl ActivityIndicator {
|
||||
fn pending_environment_errors<'a>(
|
||||
&'a self,
|
||||
cx: &'a App,
|
||||
) -> impl Iterator<Item = (&'a WorktreeId, &'a EnvironmentErrorMessage)> {
|
||||
) -> impl Iterator<Item = (&'a Arc<Path>, &'a EnvironmentErrorMessage)> {
|
||||
self.project.read(cx).shell_environment_errors(cx)
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
|
||||
// Show if any direnv calls failed
|
||||
if let Some((&worktree_id, error)) = self.pending_environment_errors(cx).next() {
|
||||
if let Some((abs_path, error)) = self.pending_environment_errors(cx).next() {
|
||||
let abs_path = abs_path.clone();
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
@@ -234,7 +235,7 @@ impl ActivityIndicator {
|
||||
message: error.0.clone(),
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.project.update(cx, |project, cx| {
|
||||
project.remove_environment_error(worktree_id, cx);
|
||||
project.remove_environment_error(&abs_path, cx);
|
||||
});
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
})),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
name = "agent"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
@@ -25,6 +25,7 @@ assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
@@ -40,7 +41,6 @@ fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
git.workspace = true
|
||||
git_ui.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
@@ -55,6 +55,7 @@ lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
@@ -64,6 +65,7 @@ prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
@@ -79,13 +81,16 @@ theme.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
vim_mode_setting.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
1040
crates/agent/src/agent_diff.rs
Normal file
1040
crates/agent/src/agent_diff.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
mod active_thread;
|
||||
mod agent_diff;
|
||||
mod assistant_configuration;
|
||||
mod assistant_model_selector;
|
||||
mod assistant_panel;
|
||||
@@ -22,14 +23,17 @@ mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings};
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{actions, App};
|
||||
use gpui::{App, actions, impl_actions};
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings as _;
|
||||
use thread::ThreadId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
@@ -37,17 +41,16 @@ pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate}
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
pub use agent_diff::{AgentDiff, AgentDiffToolbar};
|
||||
|
||||
actions!(
|
||||
assistant2,
|
||||
agent,
|
||||
[
|
||||
NewThread,
|
||||
NewPromptEditor,
|
||||
ToggleContextPicker,
|
||||
ToggleProfileSelector,
|
||||
RemoveAllContext,
|
||||
OpenHistory,
|
||||
OpenConfiguration,
|
||||
ManageProfiles,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
@@ -60,13 +63,40 @@ actions!(
|
||||
FocusRight,
|
||||
RemoveFocusedContext,
|
||||
AcceptSuggestedContext,
|
||||
OpenActiveThreadAsMarkdown
|
||||
OpenActiveThreadAsMarkdown,
|
||||
OpenAgentDiff,
|
||||
Keep,
|
||||
Reject,
|
||||
RejectAll,
|
||||
KeepAll
|
||||
]
|
||||
);
|
||||
|
||||
const NAMESPACE: &str = "assistant2";
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
}
|
||||
|
||||
/// Initializes the `assistant2` crate.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
impl ManageProfiles {
|
||||
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
|
||||
Self {
|
||||
customize_tools: Some(profile_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_actions!(agent, [NewThread, ManageProfiles]);
|
||||
|
||||
const NAMESPACE: &str = "agent";
|
||||
|
||||
/// Initializes the `agent` crate.
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
@@ -92,10 +122,10 @@ pub fn init(
|
||||
cx.observe_new(AddContextServerModal::register).detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
feature_gate_agent_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_assistant2_actions(cx: &mut App) {
|
||||
fn feature_gate_agent_actions(cx: &mut App) {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
@@ -1,16 +1,18 @@
|
||||
mod add_context_server_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod profile_picker;
|
||||
mod tool_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use fs::Fs;
|
||||
use gpui::{Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{prelude::*, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use ui::{Disclosure, Divider, DividerColor, ElevationIndex, Indicator, Switch, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
@@ -20,6 +22,7 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
use crate::AddContextServer;
|
||||
|
||||
pub struct AssistantConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
@@ -30,6 +33,7 @@ pub struct AssistantConfiguration {
|
||||
|
||||
impl AssistantConfiguration {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
window: &mut Window,
|
||||
@@ -55,6 +59,7 @@ impl AssistantConfiguration {
|
||||
);
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
focus_handle,
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_manager,
|
||||
@@ -106,7 +111,7 @@ impl AssistantConfiguration {
|
||||
&mut self,
|
||||
provider: &Arc<dyn LanguageModelProvider>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
) -> impl IntoElement + use<> {
|
||||
let provider_id = provider.id().0.clone();
|
||||
let provider_name = provider.name().0.clone();
|
||||
let configuration_view = self
|
||||
@@ -168,6 +173,55 @@ 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";
|
||||
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(Headline::new("General Settings").size(HeadlineSize::Small))
|
||||
.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()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.max_w_5_6()
|
||||
.child(Label::new(HEADING))
|
||||
.child(Label::new("When enabled, the agent can perform potentially destructive actions without asking for your confirmation.").color(Color::Muted)),
|
||||
)
|
||||
.child(
|
||||
Switch::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
always_allow_tool_actions.into(),
|
||||
)
|
||||
.on_click({
|
||||
let fs = self.fs.clone();
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _| {
|
||||
settings.set_always_allow_tool_actions(allow);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_context_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
|
||||
let tools_by_source = self.tools.tools_by_source(cx);
|
||||
@@ -359,6 +413,8 @@ impl Render for AssistantConfiguration {
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(self.render_context_servers_section(cx))
|
||||
.child(Divider::horizontal().color(DividerColor::Border))
|
||||
.child(
|
||||
@@ -1,17 +1,17 @@
|
||||
use context_server::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||
use editor::Editor;
|
||||
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity};
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, Modal, ModalFooter, ModalHeader, Section, Tooltip};
|
||||
use ui::{Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
pub struct AddContextServerModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
name_editor: Entity<Editor>,
|
||||
command_editor: Entity<Editor>,
|
||||
name_editor: Entity<SingleLineInput>,
|
||||
command_editor: Entity<SingleLineInput>,
|
||||
}
|
||||
|
||||
impl AddContextServerModal {
|
||||
@@ -33,15 +33,10 @@ impl AddContextServerModal {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
let command_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
|
||||
name_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Context server name", cx);
|
||||
});
|
||||
|
||||
command_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Command to run the context server", cx);
|
||||
let name_editor =
|
||||
cx.new(|cx| SingleLineInput::new(window, cx, "Your server name").label("Name"));
|
||||
let command_editor = cx.new(|cx| {
|
||||
SingleLineInput::new(window, cx, "Command").label("Command to run the context server")
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -52,8 +47,22 @@ impl AddContextServerModal {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut Context<Self>) {
|
||||
let name = self.name_editor.read(cx).text(cx).trim().to_string();
|
||||
let command = self.command_editor.read(cx).text(cx).trim().to_string();
|
||||
let name = self
|
||||
.name_editor
|
||||
.read(cx)
|
||||
.editor()
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.trim()
|
||||
.to_string();
|
||||
let command = self
|
||||
.command_editor
|
||||
.read(cx)
|
||||
.editor()
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if name.is_empty() || command.is_empty() {
|
||||
return;
|
||||
@@ -104,8 +113,8 @@ impl EventEmitter<DismissEvent> for AddContextServerModal {}
|
||||
|
||||
impl Render for AddContextServerModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_name_empty = self.name_editor.read(cx).text(cx).trim().is_empty();
|
||||
let is_command_empty = self.command_editor.read(cx).text(cx).trim().is_empty();
|
||||
let is_name_empty = self.name_editor.read(cx).is_empty(cx);
|
||||
let is_command_empty = self.command_editor.read(cx).is_empty(cx);
|
||||
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
@@ -122,18 +131,8 @@ impl Render for AddContextServerModal {
|
||||
.header(ModalHeader::new().headline("Add Context Server"))
|
||||
.section(
|
||||
Section::new()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new("Name"))
|
||||
.child(self.name_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new("Command"))
|
||||
.child(self.command_editor.clone()),
|
||||
),
|
||||
.child(self.name_editor.clone())
|
||||
.child(self.command_editor.clone()),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new()
|
||||
@@ -2,71 +2,76 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
|
||||
ContextServerPresetContent, VersionedAssistantSettingsContent,
|
||||
};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use settings::{update_settings_file, Settings as _};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry};
|
||||
use gpui::{
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity,
|
||||
prelude::*,
|
||||
};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use ui::{
|
||||
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
|
||||
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AssistantPanel, ManageProfiles};
|
||||
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
|
||||
|
||||
enum Mode {
|
||||
ChooseProfile {
|
||||
profile_picker: Entity<ProfilePicker>,
|
||||
_subscription: Subscription,
|
||||
},
|
||||
ChooseProfile(ChooseProfileMode),
|
||||
NewProfile(NewProfileMode),
|
||||
ViewProfile(ViewProfileMode),
|
||||
ConfigureTools {
|
||||
profile_id: Arc<str>,
|
||||
profile_id: AgentProfileId,
|
||||
tool_picker: Entity<ToolPicker>,
|
||||
_subscription: Subscription,
|
||||
},
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
pub fn choose_profile(window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
|
||||
let this = cx.entity();
|
||||
pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let profile_picker = cx.new(|cx| {
|
||||
let delegate = ProfilePickerDelegate::new(
|
||||
move |profile_id, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
},
|
||||
cx,
|
||||
);
|
||||
ProfilePicker::new(delegate, window, cx)
|
||||
});
|
||||
let dismiss_subscription = cx.subscribe_in(
|
||||
&profile_picker,
|
||||
window,
|
||||
|_this, _profile_picker, _: &DismissEvent, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
);
|
||||
let mut profiles = settings.profiles.clone();
|
||||
profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
|
||||
|
||||
Self::ChooseProfile {
|
||||
profile_picker,
|
||||
_subscription: dismiss_subscription,
|
||||
}
|
||||
let profiles = profiles
|
||||
.into_iter()
|
||||
.map(|(id, profile)| ProfileEntry {
|
||||
id,
|
||||
name: profile.name,
|
||||
navigation: NavigableEntry::focusable(cx),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self::ChooseProfile(ChooseProfileMode {
|
||||
profiles,
|
||||
add_new_profile: NavigableEntry::focusable(cx),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProfileEntry {
|
||||
pub id: AgentProfileId,
|
||||
pub name: SharedString,
|
||||
pub navigation: NavigableEntry,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ChooseProfileMode {
|
||||
profiles: Vec<ProfileEntry>,
|
||||
add_new_profile: NavigableEntry,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ViewProfileMode {
|
||||
profile_id: Arc<str>,
|
||||
profile_id: AgentProfileId,
|
||||
fork_profile: NavigableEntry,
|
||||
configure_tools: NavigableEntry,
|
||||
}
|
||||
@@ -74,12 +79,13 @@ pub struct ViewProfileMode {
|
||||
#[derive(Clone)]
|
||||
pub struct NewProfileMode {
|
||||
name_editor: Entity<Editor>,
|
||||
base_profile_id: Option<Arc<str>>,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
}
|
||||
|
||||
pub struct ManageProfilesModal {
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
mode: Mode,
|
||||
}
|
||||
@@ -90,12 +96,21 @@ impl ManageProfilesModal {
|
||||
_window: Option<&mut Window>,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
|
||||
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let thread_store = panel.read(cx).thread_store().read(cx);
|
||||
let tools = thread_store.tools();
|
||||
workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
|
||||
let thread_store = panel.read(cx).thread_store();
|
||||
let tools = thread_store.read(cx).tools();
|
||||
let thread_store = thread_store.downgrade();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let mut this = Self::new(fs, tools, thread_store, window, cx);
|
||||
|
||||
if let Some(profile_id) = action.customize_tools.clone() {
|
||||
this.configure_tools(profile_id, window, cx);
|
||||
}
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -103,6 +118,7 @@ impl ManageProfilesModal {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -111,6 +127,7 @@ impl ManageProfilesModal {
|
||||
Self {
|
||||
fs,
|
||||
tools,
|
||||
thread_store,
|
||||
focus_handle,
|
||||
mode: Mode::choose_profile(window, cx),
|
||||
}
|
||||
@@ -123,7 +140,7 @@ impl ManageProfilesModal {
|
||||
|
||||
fn new_profile(
|
||||
&mut self,
|
||||
base_profile_id: Option<Arc<str>>,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -141,7 +158,7 @@ impl ManageProfilesModal {
|
||||
|
||||
pub fn view_profile(
|
||||
&mut self,
|
||||
profile_id: Arc<str>,
|
||||
profile_id: AgentProfileId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -155,7 +172,7 @@ impl ManageProfilesModal {
|
||||
|
||||
fn configure_tools(
|
||||
&mut self,
|
||||
profile_id: Arc<str>,
|
||||
profile_id: AgentProfileId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -168,6 +185,7 @@ impl ManageProfilesModal {
|
||||
let delegate = ToolPickerDelegate::new(
|
||||
self.fs.clone(),
|
||||
self.tools.clone(),
|
||||
self.thread_store.clone(),
|
||||
profile_id.clone(),
|
||||
profile,
|
||||
cx,
|
||||
@@ -201,7 +219,7 @@ impl ManageProfilesModal {
|
||||
.and_then(|profile_id| settings.profiles.get(profile_id).cloned());
|
||||
|
||||
let name = mode.name_editor.read(cx).text(cx);
|
||||
let profile_id: Arc<str> = name.to_case(Case::Kebab).into();
|
||||
let profile_id = AgentProfileId(name.to_case(Case::Kebab).into());
|
||||
|
||||
let profile = AgentProfile {
|
||||
name: name.into(),
|
||||
@@ -209,6 +227,10 @@ impl ManageProfilesModal {
|
||||
.as_ref()
|
||||
.map(|profile| profile.tools.clone())
|
||||
.unwrap_or_default(),
|
||||
enable_all_context_servers: base_profile
|
||||
.as_ref()
|
||||
.map(|profile| profile.enable_all_context_servers)
|
||||
.unwrap_or_default(),
|
||||
context_servers: base_profile
|
||||
.map(|profile| profile.context_servers)
|
||||
.unwrap_or_default(),
|
||||
@@ -224,7 +246,9 @@ impl ManageProfilesModal {
|
||||
|
||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile { .. } => {}
|
||||
Mode::ChooseProfile { .. } => {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
Mode::NewProfile(mode) => {
|
||||
if let Some(profile_id) = mode.base_profile_id.clone() {
|
||||
self.view_profile(profile_id, window, cx);
|
||||
@@ -237,39 +261,15 @@ impl ManageProfilesModal {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_profile(&self, profile_id: Arc<str>, profile: AgentProfile, cx: &mut Context<Self>) {
|
||||
fn create_profile(
|
||||
&self,
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
move |settings, _cx| match settings {
|
||||
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
|
||||
settings,
|
||||
)) => {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
log::error!("profile with ID '{profile_id}' already exists");
|
||||
return;
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(profile_id, profile).log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -280,7 +280,7 @@ impl ModalView for ManageProfilesModal {}
|
||||
impl Focusable for ManageProfilesModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile { profile_picker, .. } => profile_picker.focus_handle(cx),
|
||||
Mode::ChooseProfile(_) => self.focus_handle.clone(),
|
||||
Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
|
||||
Mode::ViewProfile(_) => self.focus_handle.clone(),
|
||||
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
|
||||
@@ -291,15 +291,133 @@ impl Focusable for ManageProfilesModal {
|
||||
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
|
||||
|
||||
impl ManageProfilesModal {
|
||||
fn render_choose_profile(
|
||||
&mut self,
|
||||
mode: ChooseProfileMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Navigable::new(
|
||||
div()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(ProfileModalHeader::new(
|
||||
"Agent Profiles",
|
||||
IconName::ZedAssistant,
|
||||
))
|
||||
.child(
|
||||
v_flex()
|
||||
.pb_1()
|
||||
.child(ListSeparator)
|
||||
.children(mode.profiles.iter().map(|profile| {
|
||||
div()
|
||||
.id(SharedString::from(format!("profile-{}", profile.id)))
|
||||
.track_focus(&profile.navigation.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"profile-{}",
|
||||
profile.id
|
||||
)))
|
||||
.toggle_state(
|
||||
profile
|
||||
.navigation
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(profile.name.clone()))
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new("Customize").size(LabelSize::Small))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&menu::Confirm,
|
||||
&self.focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.on_click({
|
||||
let profile_id = profile.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
}),
|
||||
)
|
||||
}))
|
||||
.child(ListSeparator)
|
||||
.child(
|
||||
div()
|
||||
.id("new-profile")
|
||||
.track_focus(&mode.add_new_profile.focus_handle)
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
|
||||
this.new_profile(None, window, cx);
|
||||
}))
|
||||
.child(
|
||||
ListItem::new("new-profile")
|
||||
.toggle_state(
|
||||
mode.add_new_profile
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Plus))
|
||||
.child(Label::new("Add New Profile"))
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.new_profile(None, window, cx);
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.map(|mut navigable| {
|
||||
for profile in mode.profiles {
|
||||
navigable = navigable.entry(profile.navigation);
|
||||
}
|
||||
|
||||
navigable
|
||||
})
|
||||
.entry(mode.add_new_profile)
|
||||
}
|
||||
|
||||
fn render_new_profile(
|
||||
&mut self,
|
||||
mode: NewProfileMode,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let base_profile_name = mode.base_profile_id.as_ref().map(|base_profile_id| {
|
||||
settings
|
||||
.profiles
|
||||
.get(base_profile_id)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into())
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.id("new-profile")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.child(ProfileModalHeader::new(
|
||||
match base_profile_name {
|
||||
Some(base_profile) => format!("Fork {base_profile}"),
|
||||
None => "New Profile".into(),
|
||||
},
|
||||
IconName::Plus,
|
||||
))
|
||||
.child(ListSeparator)
|
||||
.child(h_flex().p_2().child(mode.name_editor.clone()))
|
||||
}
|
||||
|
||||
@@ -418,10 +536,8 @@ impl Render for ManageProfilesModal {
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(match &self.mode {
|
||||
Mode::ChooseProfile { profile_picker, .. } => div()
|
||||
.child(ProfileModalHeader::new("Profiles", IconName::ZedAssistant))
|
||||
.child(ListSeparator)
|
||||
.child(profile_picker.clone())
|
||||
Mode::ChooseProfile(mode) => self
|
||||
.render_choose_profile(mode.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
Mode::NewProfile(mode) => self
|
||||
.render_new_profile(mode.clone(), window, cx)
|
||||
@@ -1,18 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AgentProfileContent, AssistantSettings, AssistantSettingsContent,
|
||||
AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
|
||||
ContextServerPresetContent, VersionedAssistantSettingsContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ThreadStore;
|
||||
|
||||
pub struct ToolPicker {
|
||||
picker: Entity<Picker<ToolPickerDelegate>>,
|
||||
}
|
||||
@@ -46,9 +48,10 @@ pub struct ToolEntry {
|
||||
|
||||
pub struct ToolPickerDelegate {
|
||||
tool_picker: WeakEntity<ToolPicker>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Vec<ToolEntry>,
|
||||
profile_id: Arc<str>,
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
@@ -58,7 +61,8 @@ impl ToolPickerDelegate {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tool_set: Arc<ToolWorkingSet>,
|
||||
profile_id: Arc<str>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
cx: &mut Context<ToolPicker>,
|
||||
) -> Self {
|
||||
@@ -73,6 +77,7 @@ impl ToolPickerDelegate {
|
||||
|
||||
Self {
|
||||
tool_picker: cx.entity().downgrade(),
|
||||
thread_store,
|
||||
fs,
|
||||
tools: tool_entries,
|
||||
profile_id,
|
||||
@@ -183,45 +188,57 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
}
|
||||
};
|
||||
|
||||
let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
|
||||
if active_profile_id == &self.profile_id {
|
||||
self.thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile(&self.profile, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let default_profile = self.profile.clone();
|
||||
let tool = tool.clone();
|
||||
move |settings, _cx| match settings {
|
||||
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
|
||||
settings,
|
||||
)) => {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
AssistantSettingsContent::Versioned(boxed) => {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
let profile =
|
||||
profiles
|
||||
.entry(profile_id)
|
||||
.or_insert_with(|| AgentProfileContent {
|
||||
name: default_profile.name.into(),
|
||||
tools: default_profile.tools,
|
||||
enable_all_context_servers: Some(
|
||||
default_profile.enable_all_context_servers,
|
||||
),
|
||||
context_servers: default_profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
|
||||
match tool.source {
|
||||
ToolSource::Native => {
|
||||
*profile.tools.entry(tool.name).or_default() = is_enabled;
|
||||
}
|
||||
ToolSource::ContextServer { id } => {
|
||||
let preset = profile
|
||||
.context_servers
|
||||
.entry(id.clone().into())
|
||||
.or_default();
|
||||
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
|
||||
match tool.source {
|
||||
ToolSource::Native => {
|
||||
*profile.tools.entry(tool.name).or_default() = is_enabled;
|
||||
}
|
||||
ToolSource::ContextServer { id } => {
|
||||
let preset = profile
|
||||
.context_servers
|
||||
.entry(id.clone().into())
|
||||
.or_default();
|
||||
*preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,19 @@ use language_model_selector::{
|
||||
};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ButtonLike, PopoverMenuHandle, Tooltip};
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ModelType {
|
||||
Default,
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
pub struct AssistantModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
}
|
||||
|
||||
impl AssistantModelSelector {
|
||||
@@ -20,6 +27,7 @@ impl AssistantModelSelector {
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
model_type: ModelType,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
@@ -28,11 +36,32 @@ impl AssistantModelSelector {
|
||||
let fs = fs.clone();
|
||||
LanguageModelSelector::new(
|
||||
move |model, cx| {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| settings.set_model(model.clone()),
|
||||
);
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
|
||||
match model_type {
|
||||
ModelType::Default => {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings.set_model(model.clone());
|
||||
},
|
||||
);
|
||||
}
|
||||
ModelType::InlineAssistant => {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings.set_inline_assistant_model(
|
||||
provider.clone(),
|
||||
model_id.clone(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
@@ -40,6 +69,7 @@ impl AssistantModelSelector {
|
||||
}),
|
||||
menu_handle,
|
||||
focus_handle,
|
||||
model_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +80,16 @@ impl AssistantModelSelector {
|
||||
|
||||
impl Render for AssistantModelSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
|
||||
let model = match self.model_type {
|
||||
ModelType::Default => model_registry.default_model(),
|
||||
ModelType::InlineAssistant => model_registry.inline_assistant_model(),
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
let model_name = match model {
|
||||
Some(model) => model.model.name().0,
|
||||
_ => SharedString::from("No model selected"),
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,12 @@ use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{channel::mpsc, future::LocalBoxFuture, join, SinkExt, Stream, StreamExt};
|
||||
use futures::{SinkExt, Stream, StreamExt, channel::mpsc, future::LocalBoxFuture, join};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
|
||||
use language::{line_diff, Buffer, IndentKind, Point, TransactionId};
|
||||
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
@@ -156,8 +156,9 @@ impl BufferCodegen {
|
||||
}
|
||||
|
||||
let primary_model = LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.context("no active model")?;
|
||||
.default_model()
|
||||
.context("no active model")?
|
||||
.model;
|
||||
|
||||
for (model, alternative) in iter::once(primary_model)
|
||||
.chain(alternative_models)
|
||||
@@ -414,7 +415,11 @@ impl CodegenAlternative {
|
||||
};
|
||||
|
||||
if let Some(context_store) = &self.context_store {
|
||||
attach_context_to_message(&mut request_message, context_store.read(cx).snapshot(cx));
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
@@ -1028,14 +1033,14 @@ impl Diff {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{
|
||||
stream::{self},
|
||||
Stream,
|
||||
stream::{self},
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
|
||||
tree_sitter_rust,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
244
crates/agent/src/context.rs
Normal file
244
crates/agent/src/context.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use language::{Buffer, File};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::ProjectPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::{Anchor, BufferId};
|
||||
use ui::IconName;
|
||||
use util::post_inc;
|
||||
|
||||
use crate::thread::Thread;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
Symbol,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
|
||||
impl ContextKind {
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::Symbol => IconName::Code,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AssistantContext {
|
||||
File(FileContext),
|
||||
Directory(DirectoryContext),
|
||||
Symbol(SymbolContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
pub fn id(&self) -> ContextId {
|
||||
match self {
|
||||
Self::File(file) => file.id,
|
||||
Self::Directory(directory) => directory.id,
|
||||
Self::Symbol(symbol) => symbol.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileContext {
|
||||
pub id: ContextId,
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectoryContext {
|
||||
pub id: ContextId,
|
||||
pub project_path: ProjectPath,
|
||||
pub context_buffers: Vec<ContextBuffer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SymbolContext {
|
||||
pub id: ContextId,
|
||||
pub context_symbol: ContextSymbol,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FetchedUrlContext {
|
||||
pub id: ContextId,
|
||||
pub url: SharedString,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
||||
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadContext {
|
||||
pub id: ContextId,
|
||||
pub thread: Entity<Thread>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl ThreadContext {
|
||||
pub fn summary(&self, cx: &App) -> SharedString {
|
||||
self.thread
|
||||
.read(cx)
|
||||
.summary()
|
||||
.unwrap_or("New thread".into())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
||||
// the context from the message editor in this case.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ContextBuffer {
|
||||
pub id: BufferId,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub file: Arc<dyn File>,
|
||||
pub version: clock::Global,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ContextBuffer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ContextBuffer")
|
||||
.field("id", &self.id)
|
||||
.field("buffer", &self.buffer)
|
||||
.field("version", &self.version)
|
||||
.field("text", &self.text)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextSymbol {
|
||||
pub id: ContextSymbolId,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub buffer_version: clock::Global,
|
||||
/// The range that the symbol encloses, e.g. for function symbol, this will
|
||||
/// include not only the signature, but also the body
|
||||
pub enclosing_range: Range<Anchor>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ContextSymbolId {
|
||||
pub path: ProjectPath,
|
||||
pub name: SharedString,
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
/// Formats a collection of contexts into a string representation
|
||||
pub fn format_context_as_string<'a>(
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
cx: &App,
|
||||
) -> Option<String> {
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
let mut symbol_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
|
||||
for context in contexts {
|
||||
match context {
|
||||
AssistantContext::File(context) => file_context.push(context),
|
||||
AssistantContext::Directory(context) => directory_context.push(context),
|
||||
AssistantContext::Symbol(context) => symbol_context.push(context),
|
||||
AssistantContext::FetchedUrl(context) => fetch_context.push(context),
|
||||
AssistantContext::Thread(context) => thread_context.push(context),
|
||||
}
|
||||
}
|
||||
|
||||
if file_context.is_empty()
|
||||
&& directory_context.is_empty()
|
||||
&& symbol_context.is_empty()
|
||||
&& fetch_context.is_empty()
|
||||
&& thread_context.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = String::new();
|
||||
result.push_str("\n<context>\n\
|
||||
The following items were attached by the user. You don't need to use other tools to read them.\n\n");
|
||||
|
||||
if !file_context.is_empty() {
|
||||
result.push_str("<files>\n");
|
||||
for context in file_context {
|
||||
result.push_str(&context.context_buffer.text);
|
||||
}
|
||||
result.push_str("</files>\n");
|
||||
}
|
||||
|
||||
if !directory_context.is_empty() {
|
||||
result.push_str("<directories>\n");
|
||||
for context in directory_context {
|
||||
for context_buffer in &context.context_buffers {
|
||||
result.push_str(&context_buffer.text);
|
||||
}
|
||||
}
|
||||
result.push_str("</directories>\n");
|
||||
}
|
||||
|
||||
if !symbol_context.is_empty() {
|
||||
result.push_str("<symbols>\n");
|
||||
for context in symbol_context {
|
||||
result.push_str(&context.context_symbol.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</symbols>\n");
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
result.push_str("<fetched_urls>\n");
|
||||
for context in &fetch_context {
|
||||
result.push_str(&context.url);
|
||||
result.push('\n');
|
||||
result.push_str(&context.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</fetched_urls>\n");
|
||||
}
|
||||
|
||||
if !thread_context.is_empty() {
|
||||
result.push_str("<conversation_threads>\n");
|
||||
for context in &thread_context {
|
||||
result.push_str(&context.summary(cx));
|
||||
result.push('\n');
|
||||
result.push_str(&context.text);
|
||||
result.push('\n');
|
||||
}
|
||||
result.push_str("</conversation_threads>\n");
|
||||
}
|
||||
|
||||
result.push_str("</context>\n");
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn attach_context_to_message<'a>(
|
||||
message: &mut LanguageModelRequestMessage,
|
||||
contexts: impl Iterator<Item = &'a AssistantContext>,
|
||||
cx: &App,
|
||||
) {
|
||||
if let Some(context_string) = format_context_as_string(contexts, cx) {
|
||||
message.content.push(context_string.into());
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,38 @@
|
||||
mod completion_provider;
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod symbol_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use editor::display_map::{Crease, FoldId};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
|
||||
use file_context_picker::render_file_context_entry;
|
||||
use gpui::{
|
||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity,
|
||||
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::ProjectPath;
|
||||
use thread_context_picker::{render_thread_context_entry, ThreadContextEntry};
|
||||
use project::{Entry, ProjectPath};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{ThreadContextEntry, render_thread_context_entry};
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor,
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
pub use crate::context_picker::completion_provider::ContextPickerCompletionProvider;
|
||||
use crate::context_picker::fetch_context_picker::FetchContextPicker;
|
||||
use crate::context_picker::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::ThreadId;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::AssistantPanel;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ConfirmBehavior {
|
||||
@@ -39,6 +43,7 @@ pub enum ConfirmBehavior {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ContextPickerMode {
|
||||
File,
|
||||
Symbol,
|
||||
Fetch,
|
||||
Thread,
|
||||
}
|
||||
@@ -49,6 +54,7 @@ impl TryFrom<&str> for ContextPickerMode {
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
"file" => Ok(Self::File),
|
||||
"symbol" => Ok(Self::Symbol),
|
||||
"fetch" => Ok(Self::Fetch),
|
||||
"thread" => Ok(Self::Thread),
|
||||
_ => Err(format!("Invalid context picker mode: {}", value)),
|
||||
@@ -60,6 +66,7 @@ impl ContextPickerMode {
|
||||
pub fn mention_prefix(&self) -> &'static str {
|
||||
match self {
|
||||
Self::File => "file",
|
||||
Self::Symbol => "symbol",
|
||||
Self::Fetch => "fetch",
|
||||
Self::Thread => "thread",
|
||||
}
|
||||
@@ -68,16 +75,18 @@ impl ContextPickerMode {
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::File => "Files & Directories",
|
||||
Self::Symbol => "Symbols",
|
||||
Self::Fetch => "Fetch",
|
||||
Self::Thread => "Thread",
|
||||
Self::Thread => "Threads",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
Self::File => IconName::File,
|
||||
Self::Symbol => IconName::Code,
|
||||
Self::Fetch => IconName::Globe,
|
||||
Self::Thread => IconName::MessageCircle,
|
||||
Self::Thread => IconName::MessageBubbles,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +95,7 @@ impl ContextPickerMode {
|
||||
enum ContextPickerState {
|
||||
Default(Entity<ContextMenu>),
|
||||
File(Entity<FileContextPicker>),
|
||||
Symbol(Entity<SymbolContextPicker>),
|
||||
Fetch(Entity<FetchContextPicker>),
|
||||
Thread(Entity<ThreadContextPicker>),
|
||||
}
|
||||
@@ -96,6 +106,7 @@ pub(super) struct ContextPicker {
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ContextPicker {
|
||||
@@ -107,6 +118,22 @@ impl ContextPicker {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = context_store
|
||||
.upgrade()
|
||||
.map(|context_store| {
|
||||
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
|
||||
})
|
||||
.into_iter()
|
||||
.chain(
|
||||
thread_store
|
||||
.as_ref()
|
||||
.and_then(|thread_store| thread_store.upgrade())
|
||||
.map(|thread_store| {
|
||||
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
|
||||
}),
|
||||
)
|
||||
.collect::<Vec<Subscription>>();
|
||||
|
||||
ContextPicker {
|
||||
mode: ContextPickerState::Default(ContextMenu::build(
|
||||
window,
|
||||
@@ -117,6 +144,7 @@ impl ContextPicker {
|
||||
context_store,
|
||||
thread_store,
|
||||
confirm_behavior,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +233,18 @@ impl ContextPicker {
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Symbol => {
|
||||
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
|
||||
SymbolContextPicker::new(
|
||||
context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_store.clone(),
|
||||
self.confirm_behavior,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
}
|
||||
ContextPickerMode::Fetch => {
|
||||
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
|
||||
FetchContextPicker::new(
|
||||
@@ -339,73 +379,25 @@ impl ContextPicker {
|
||||
}
|
||||
|
||||
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
|
||||
let Some(workspace) = self.workspace.upgrade().map(|w| w.read(cx)) else {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let Some(context_store) = self.context_store.upgrade().map(|cs| cs.read(cx)) else {
|
||||
let Some(context_store) = self.context_store.upgrade() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut recent = Vec::with_capacity(6);
|
||||
recent_context_picker_entries(context_store, self.thread_store.clone(), workspace, cx)
|
||||
}
|
||||
|
||||
let mut current_files = context_store.file_paths(cx);
|
||||
|
||||
if let Some(active_path) = active_singleton_buffer_path(&workspace, cx) {
|
||||
current_files.insert(active_path);
|
||||
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
|
||||
match &self.mode {
|
||||
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
|
||||
}
|
||||
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
recent.extend(
|
||||
workspace
|
||||
.recent_navigation_history_iter(cx)
|
||||
.filter(|(path, _)| !current_files.contains(&path.path.to_path_buf()))
|
||||
.take(4)
|
||||
.filter_map(|(project_path, _)| {
|
||||
project
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
.map(|worktree| RecentEntry::File {
|
||||
project_path,
|
||||
path_prefix: worktree.read(cx).root_name().into(),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
let mut current_threads = context_store.thread_ids();
|
||||
|
||||
if let Some(active_thread) = workspace
|
||||
.panel::<AssistantPanel>(cx)
|
||||
.map(|panel| panel.read(cx).active_thread(cx))
|
||||
{
|
||||
current_threads.insert(active_thread.read(cx).id().clone());
|
||||
}
|
||||
|
||||
let Some(thread_store) = self
|
||||
.thread_store
|
||||
.as_ref()
|
||||
.and_then(|thread_store| thread_store.upgrade())
|
||||
else {
|
||||
return recent;
|
||||
};
|
||||
|
||||
thread_store.update(cx, |thread_store, _cx| {
|
||||
recent.extend(
|
||||
thread_store
|
||||
.threads()
|
||||
.into_iter()
|
||||
.filter(|thread| !current_threads.contains(&thread.id))
|
||||
.take(2)
|
||||
.map(|thread| {
|
||||
RecentEntry::Thread(ThreadContextEntry {
|
||||
id: thread.id,
|
||||
summary: thread.summary,
|
||||
})
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
recent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,6 +408,7 @@ impl Focusable for ContextPicker {
|
||||
match &self.mode {
|
||||
ContextPickerState::Default(menu) => menu.focus_handle(cx),
|
||||
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
|
||||
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
|
||||
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
|
||||
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
|
||||
}
|
||||
@@ -430,6 +423,7 @@ impl Render for ContextPicker {
|
||||
.map(|parent| match &self.mode {
|
||||
ContextPickerState::Default(menu) => parent.child(menu.clone()),
|
||||
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
|
||||
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
|
||||
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
|
||||
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
|
||||
})
|
||||
@@ -446,23 +440,17 @@ enum RecentEntry {
|
||||
fn supported_context_picker_modes(
|
||||
thread_store: &Option<WeakEntity<ThreadStore>>,
|
||||
) -> Vec<ContextPickerMode> {
|
||||
let mut modes = vec![ContextPickerMode::File, ContextPickerMode::Fetch];
|
||||
let mut modes = vec![
|
||||
ContextPickerMode::File,
|
||||
ContextPickerMode::Symbol,
|
||||
ContextPickerMode::Fetch,
|
||||
];
|
||||
if thread_store.is_some() {
|
||||
modes.push(ContextPickerMode::Thread);
|
||||
}
|
||||
modes
|
||||
}
|
||||
|
||||
fn active_singleton_buffer_path(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||
let active_item = workspace.active_item(cx)?;
|
||||
|
||||
let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
|
||||
let buffer = editor.buffer().read(cx).as_singleton()?;
|
||||
|
||||
let path = buffer.read(cx).file()?.path().to_path_buf();
|
||||
Some(path)
|
||||
}
|
||||
|
||||
fn recent_context_picker_entries(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
@@ -471,14 +459,8 @@ fn recent_context_picker_entries(
|
||||
) -> Vec<RecentEntry> {
|
||||
let mut recent = Vec::with_capacity(6);
|
||||
|
||||
let mut current_files = context_store.read(cx).file_paths(cx);
|
||||
|
||||
let current_files = context_store.read(cx).file_paths(cx);
|
||||
let workspace = workspace.read(cx);
|
||||
|
||||
if let Some(active_path) = active_singleton_buffer_path(workspace, cx) {
|
||||
current_files.insert(active_path);
|
||||
}
|
||||
|
||||
let project = workspace.project().read(cx);
|
||||
|
||||
recent.extend(
|
||||
@@ -542,6 +524,7 @@ pub(crate) fn insert_crease_for_mention(
|
||||
return;
|
||||
};
|
||||
|
||||
let start = start.bias_right(&snapshot);
|
||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||
|
||||
let placeholder = FoldPlaceholder {
|
||||
@@ -651,3 +634,93 @@ fn fold_toggle(
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
}
|
||||
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
const SYMBOL: &str = "@symbol";
|
||||
const THREAD: &str = "@thread";
|
||||
const FETCH: &str = "@fetch";
|
||||
|
||||
const SEPARATOR: &str = ":";
|
||||
|
||||
pub fn is_valid(url: &str) -> bool {
|
||||
url.starts_with(Self::FILE)
|
||||
|| url.starts_with(Self::SYMBOL)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
||||
}
|
||||
|
||||
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
|
||||
format!(
|
||||
"[@{}]({}:{}:{})",
|
||||
symbol_name,
|
||||
Self::SYMBOL,
|
||||
full_path,
|
||||
symbol_name
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_fetch(url: &str) -> String {
|
||||
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
||||
}
|
||||
|
||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||
}
|
||||
|
||||
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
|
||||
fn extract_project_path_from_link(
|
||||
path: &str,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &App,
|
||||
) -> Option<ProjectPath> {
|
||||
let path = PathBuf::from(path);
|
||||
let worktree_name = path.iter().next()?;
|
||||
let path: PathBuf = path.iter().skip(1).collect();
|
||||
let worktree_id = workspace
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.find(|worktree| worktree.read(cx).root_name() == worktree_name)
|
||||
.map(|worktree| worktree.read(cx).id())?;
|
||||
Some(ProjectPath {
|
||||
worktree_id,
|
||||
path: path.into(),
|
||||
})
|
||||
}
|
||||
|
||||
let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
|
||||
match prefix {
|
||||
Self::FILE => {
|
||||
let project_path = extract_project_path_from_link(argument, workspace, cx)?;
|
||||
let entry = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)?;
|
||||
Some(MentionLink::File(project_path, entry))
|
||||
}
|
||||
Self::SYMBOL => {
|
||||
let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
||||
}
|
||||
Self::THREAD => {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
@@ -12,7 +12,7 @@ use gpui::{App, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, ProjectPath, WorktreeId};
|
||||
use project::{Completion, CompletionIntent, ProjectPath, Symbol, WorktreeId};
|
||||
use rope::Point;
|
||||
use text::{Anchor, ToPoint};
|
||||
use ui::prelude::*;
|
||||
@@ -24,7 +24,9 @@ use crate::thread_store::ThreadStore;
|
||||
|
||||
use super::fetch_context_picker::fetch_url_content;
|
||||
use super::thread_context_picker::ThreadContextEntry;
|
||||
use super::{recent_context_picker_entries, supported_context_picker_modes, ContextPickerMode};
|
||||
use super::{
|
||||
ContextPickerMode, MentionLink, recent_context_picker_entries, supported_context_picker_modes,
|
||||
};
|
||||
|
||||
pub struct ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -152,9 +154,9 @@ impl ContextPickerCompletionProvider {
|
||||
let icon_for_completion = if recent {
|
||||
IconName::HistoryRerun
|
||||
} else {
|
||||
IconName::MessageCircle
|
||||
IconName::MessageBubbles
|
||||
};
|
||||
let new_text = format!("@thread {}", thread_entry.summary);
|
||||
let new_text = MentionLink::for_thread(&thread_entry);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
old_range: source_range.clone(),
|
||||
@@ -164,7 +166,7 @@ impl ContextPickerCompletionProvider {
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(icon_for_completion.path().into()),
|
||||
confirm: Some(confirm_completion_callback(
|
||||
IconName::MessageCircle.path().into(),
|
||||
IconName::MessageBubbles.path().into(),
|
||||
thread_entry.summary.clone(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
@@ -198,7 +200,7 @@ impl ContextPickerCompletionProvider {
|
||||
context_store: Entity<ContextStore>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
) -> Completion {
|
||||
let new_text = format!("@fetch {}", url_to_fetch);
|
||||
let new_text = MentionLink::for_fetch(&url_to_fetch);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
old_range: source_range.clone(),
|
||||
@@ -230,8 +232,8 @@ impl ContextPickerCompletionProvider {
|
||||
url_to_fetch.to_string(),
|
||||
))
|
||||
.await?;
|
||||
context_store.update(cx, |context_store, _| {
|
||||
context_store.add_fetched_url(url_to_fetch.to_string(), content)
|
||||
context_store.update(cx, |context_store, cx| {
|
||||
context_store.add_fetched_url(url_to_fetch.to_string(), content, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
@@ -279,7 +281,7 @@ impl ContextPickerCompletionProvider {
|
||||
crease_icon_path.clone()
|
||||
};
|
||||
|
||||
let new_text = format!("@file {}", full_path);
|
||||
let new_text = MentionLink::for_file(&file_name, &full_path);
|
||||
let new_text_len = new_text.len();
|
||||
Completion {
|
||||
old_range: source_range.clone(),
|
||||
@@ -308,6 +310,71 @@ impl ContextPickerCompletionProvider {
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_for_symbol(
|
||||
symbol: Symbol,
|
||||
excerpt_id: ExcerptId,
|
||||
source_range: Range<Anchor>,
|
||||
editor: Entity<Editor>,
|
||||
context_store: Entity<ContextStore>,
|
||||
workspace: Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Option<Completion> {
|
||||
let path_prefix = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktree_for_id(symbol.path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.root_name();
|
||||
|
||||
let (file_name, directory) = super::file_context_picker::extract_file_name_and_directory(
|
||||
&symbol.path.path,
|
||||
path_prefix,
|
||||
);
|
||||
let full_path = if let Some(directory) = directory {
|
||||
format!("{}{}", directory, file_name)
|
||||
} else {
|
||||
file_name.to_string()
|
||||
};
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::plain(symbol.name.clone(), None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&file_name, comment_id);
|
||||
|
||||
let new_text = MentionLink::for_symbol(&symbol.name, &full_path);
|
||||
let new_text_len = new_text.len();
|
||||
Some(Completion {
|
||||
old_range: source_range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(IconName::Code.path().into()),
|
||||
confirm: Some(confirm_completion_callback(
|
||||
IconName::Code.path().into(),
|
||||
symbol.name.clone().into(),
|
||||
excerpt_id,
|
||||
source_range.start,
|
||||
new_text_len,
|
||||
editor.clone(),
|
||||
move |cx| {
|
||||
let symbol = symbol.clone();
|
||||
let context_store = context_store.clone();
|
||||
let workspace = workspace.clone();
|
||||
super::symbol_context_picker::add_symbol(
|
||||
symbol.clone(),
|
||||
false,
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
@@ -340,7 +407,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let source_range = snapshot.anchor_after(state.source_range.start)
|
||||
let source_range = snapshot.anchor_before(state.source_range.start)
|
||||
..snapshot.anchor_before(state.source_range.end);
|
||||
|
||||
let thread_store = self.thread_store.clone();
|
||||
@@ -350,14 +417,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
cx.spawn(async move |_, cx| {
|
||||
let mut completions = Vec::new();
|
||||
|
||||
let MentionCompletion {
|
||||
mode: category,
|
||||
argument,
|
||||
..
|
||||
} = state;
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
match category {
|
||||
match mode {
|
||||
Some(ContextPickerMode::File) => {
|
||||
let path_matches = cx
|
||||
.update(|cx| {
|
||||
@@ -392,6 +455,35 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Symbol) => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
let symbol_matches = cx
|
||||
.update(|cx| {
|
||||
super::symbol_context_picker::search_symbols(
|
||||
query,
|
||||
Arc::new(AtomicBool::default()),
|
||||
&workspace,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
cx.update(|cx| {
|
||||
completions.extend(symbol_matches.into_iter().filter_map(
|
||||
|(_, symbol)| {
|
||||
Self::completion_for_symbol(
|
||||
symbol,
|
||||
excerpt_id,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
));
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Some(ContextPickerMode::Fetch) => {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
if !query.is_empty() {
|
||||
@@ -561,6 +653,15 @@ impl MentionCompletion {
|
||||
if last_mention_start >= line.len() {
|
||||
return Some(Self::default());
|
||||
}
|
||||
if last_mention_start > 0
|
||||
&& line
|
||||
.chars()
|
||||
.nth(last_mention_start - 1)
|
||||
.map_or(false, |c| !c.is_whitespace())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let rest_of_line = &line[last_mention_start + 1..];
|
||||
|
||||
let mut mode = None;
|
||||
@@ -661,6 +762,8 @@ mod tests {
|
||||
argument: Some("main.rs".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse("test@", 0), None);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -759,7 +862,7 @@ mod tests {
|
||||
.expect("Opened test file wasn't an editor")
|
||||
});
|
||||
|
||||
let context_store = cx.new(|_| ContextStore::new(workspace.downgrade()));
|
||||
let context_store = cx.new(|_| ContextStore::new(workspace.downgrade(), None));
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -787,11 +890,12 @@ mod tests {
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
"editor dir/",
|
||||
"seven.txt dir/b/",
|
||||
"six.txt dir/b/",
|
||||
"five.txt dir/b/",
|
||||
"four.txt dir/a/",
|
||||
"Files & Directories",
|
||||
"Symbols",
|
||||
"Fetch"
|
||||
]
|
||||
);
|
||||
@@ -828,44 +932,50 @@ mod tests {
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt",);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)",);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 25)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt ",);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ",);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 25)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_input("Ipsum ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum ",);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 25)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_input("@file ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem @file dir/a/one.txt Ipsum @file ",);
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 25)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -878,14 +988,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 25),
|
||||
Point::new(0, 32)..Point::new(0, 53)
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -895,14 +1005,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n@"
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 25),
|
||||
Point::new(0, 32)..Point::new(0, 53)
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -916,15 +1026,15 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem @file dir/a/one.txt Ipsum @file dir/b/seven.txt\n@file dir/b/six.txt"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n[@seven.txt](@file:dir/b/seven.txt)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 25),
|
||||
Point::new(0, 32)..Point::new(0, 53),
|
||||
Point::new(1, 0)..Point::new(1, 19)
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71),
|
||||
Point::new(1, 0)..Point::new(1, 35)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -2,13 +2,13 @@ use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, Context, ListItem, Window};
|
||||
use ui::{Context, ListItem, Window, prelude::*};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
@@ -163,11 +163,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
if self.url.is_empty() {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
if self.url.is_empty() { 0 } else { 1 }
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
|
||||
@@ -217,8 +213,8 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate
|
||||
.context_store
|
||||
.update(cx, |context_store, _cx| {
|
||||
context_store.add_fetched_url(url, text);
|
||||
.update(cx, |context_store, cx| {
|
||||
context_store.add_fetched_url(url, text, cx)
|
||||
})?;
|
||||
|
||||
match confirm_behavior {
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use fuzzy::PathMatch;
|
||||
@@ -9,9 +9,9 @@ use gpui::{
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
|
||||
use ui::{prelude::*, ListItem, Tooltip};
|
||||
use ui::{ListItem, Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{ContextStore, FileInclusion};
|
||||
@@ -321,10 +321,7 @@ pub fn render_file_context_entry(
|
||||
|
||||
let added = context_store.upgrade().and_then(|context_store| {
|
||||
if is_directory {
|
||||
context_store
|
||||
.read(cx)
|
||||
.includes_directory(path)
|
||||
.map(FileInclusion::Direct)
|
||||
context_store.read(cx).includes_directory(path)
|
||||
} else {
|
||||
context_store.read(cx).will_include_file_path(path, cx)
|
||||
}
|
||||
438
crates/agent/src/context_picker/symbol_context_picker.rs
Normal file
438
crates/agent/src/context_picker/symbol_context_picker.rs
Normal file
@@ -0,0 +1,438 @@
|
||||
use std::cmp::Reverse;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{DocumentSymbol, Symbol};
|
||||
use text::OffsetRangeExt;
|
||||
use ui::{ListItem, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
|
||||
pub struct SymbolContextPicker {
|
||||
picker: Entity<Picker<SymbolContextPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl SymbolContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let delegate = SymbolContextPickerDelegate::new(
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
);
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for SymbolContextPicker {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SymbolContextPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.picker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SymbolContextPickerDelegate {
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
matches: Vec<SymbolEntry>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl SymbolContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakEntity<ContextPicker>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
confirm_behavior: ConfirmBehavior,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_store,
|
||||
confirm_behavior,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search symbols…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(());
|
||||
};
|
||||
|
||||
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
|
||||
let context_store = self.context_store.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let symbols = search_task
|
||||
.await
|
||||
.context("Failed to load symbols")
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
|
||||
let symbol_entries = context_store
|
||||
.read_with(cx, |context_store, cx| {
|
||||
compute_symbol_entries(symbols, context_store, cx)
|
||||
})
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.matches = symbol_entries;
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let Some(mat) = self.matches.get(self.selected_index) else {
|
||||
return;
|
||||
};
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let confirm_behavior = self.confirm_behavior;
|
||||
let add_symbol_task = add_symbol(
|
||||
mat.symbol.clone(),
|
||||
true,
|
||||
workspace,
|
||||
self.context_store.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let selected_index = self.selected_index;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let included = add_symbol_task.await?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
|
||||
mat.is_included = included;
|
||||
}
|
||||
match confirm_behavior {
|
||||
ConfirmBehavior::KeepOpen => {}
|
||||
ConfirmBehavior::Close => this.delegate.dismissed(window, cx),
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.context_picker
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_symbol_context_entry(
|
||||
ElementId::NamedInteger("symbol-ctx-picker".into(), ix),
|
||||
mat,
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SymbolEntry {
|
||||
pub symbol: Symbol,
|
||||
pub is_included: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn add_symbol(
|
||||
symbol: Symbol,
|
||||
remove_if_exists: bool,
|
||||
workspace: Entity<Workspace>,
|
||||
context_store: WeakEntity<ContextStore>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<bool>> {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let open_buffer_task = project.update(cx, |project, cx| {
|
||||
project.open_buffer(symbol.path.clone(), cx)
|
||||
});
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = open_buffer_task.await?;
|
||||
let document_symbols = project
|
||||
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
|
||||
.await?;
|
||||
|
||||
// Try to find a matching document symbol. Document symbols include
|
||||
// not only the symbol itself (e.g. function name), but they also
|
||||
// include the context that they contain (e.g. function body).
|
||||
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
|
||||
name,
|
||||
range,
|
||||
selection_range,
|
||||
..
|
||||
}) =
|
||||
find_matching_symbol(&symbol, document_symbols.as_slice())
|
||||
{
|
||||
(name, selection_range, range)
|
||||
} else {
|
||||
// If we do not find a matching document symbol, fall back to
|
||||
// just the symbol itself
|
||||
(symbol.name, symbol.range.clone(), symbol.range)
|
||||
};
|
||||
|
||||
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
|
||||
(
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
buffer.anchor_after(enclosing_range.start)
|
||||
..buffer.anchor_before(enclosing_range.end),
|
||||
)
|
||||
})?;
|
||||
|
||||
context_store
|
||||
.update(cx, move |context_store, cx| {
|
||||
context_store.add_symbol(
|
||||
buffer,
|
||||
name.into(),
|
||||
range,
|
||||
enclosing_range,
|
||||
remove_if_exists,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
|
||||
let mut candidates = candidates.iter();
|
||||
let mut candidate = candidates.next()?;
|
||||
|
||||
loop {
|
||||
if candidate.range.start > symbol.range.end {
|
||||
return None;
|
||||
}
|
||||
if candidate.range.end < symbol.range.start {
|
||||
candidate = candidates.next()?;
|
||||
continue;
|
||||
}
|
||||
if candidate.selection_range == symbol.range {
|
||||
return Some(candidate.clone());
|
||||
}
|
||||
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
|
||||
candidates = candidate.children.iter();
|
||||
candidate = candidates.next()?;
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn search_symbols(
|
||||
query: String,
|
||||
cancellation_flag: Arc<AtomicBool>,
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<(StringMatch, Symbol)>>> {
|
||||
let symbols_task = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, cx| project.symbols(&query, cx))
|
||||
});
|
||||
let project = workspace.read(cx).project().clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let symbols = symbols_task.await?;
|
||||
let (visible_match_candidates, external_match_candidates): (Vec<_>, Vec<_>) = project
|
||||
.update(cx, |project, cx| {
|
||||
symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.label.filter_text()))
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
})
|
||||
})?;
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
&visible_match_candidates,
|
||||
&query,
|
||||
false,
|
||||
MAX_MATCHES,
|
||||
&cancellation_flag,
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
&external_match_candidates,
|
||||
&query,
|
||||
false,
|
||||
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||
&cancellation_flag,
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let sort_key_for_match = |mat: &StringMatch| {
|
||||
let symbol = &symbols[mat.candidate_id];
|
||||
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
|
||||
};
|
||||
|
||||
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
||||
external_matches.sort_unstable_by_key(sort_key_for_match);
|
||||
let mut matches = visible_matches;
|
||||
matches.append(&mut external_matches);
|
||||
|
||||
Ok(matches
|
||||
.into_iter()
|
||||
.map(|mut mat| {
|
||||
let symbol = symbols[mat.candidate_id].clone();
|
||||
let filter_start = symbol.label.filter_range.start;
|
||||
for position in &mut mat.positions {
|
||||
*position += filter_start;
|
||||
}
|
||||
(mat, symbol)
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_symbol_entries(
|
||||
symbols: Vec<(StringMatch, Symbol)>,
|
||||
context_store: &ContextStore,
|
||||
cx: &App,
|
||||
) -> Vec<SymbolEntry> {
|
||||
let mut symbol_entries = Vec::with_capacity(symbols.len());
|
||||
for (_, symbol) in symbols {
|
||||
let symbols_for_path = context_store.included_symbols_by_path().get(&symbol.path);
|
||||
let is_included = if let Some(symbols_for_path) = symbols_for_path {
|
||||
let mut is_included = false;
|
||||
for included_symbol_id in symbols_for_path {
|
||||
if included_symbol_id.name.as_ref() == symbol.name.as_str() {
|
||||
if let Some(buffer) = context_store.buffer_for_symbol(included_symbol_id) {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let included_symbol_range =
|
||||
included_symbol_id.range.to_point_utf16(&snapshot);
|
||||
|
||||
if included_symbol_range.start == symbol.range.start.0
|
||||
&& included_symbol_range.end == symbol.range.end.0
|
||||
{
|
||||
is_included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is_included
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
symbol_entries.push(SymbolEntry {
|
||||
symbol,
|
||||
is_included,
|
||||
})
|
||||
}
|
||||
symbol_entries
|
||||
}
|
||||
|
||||
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
|
||||
let path = entry
|
||||
.symbol
|
||||
.path
|
||||
.path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy())
|
||||
.unwrap_or_default();
|
||||
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
|
||||
|
||||
h_flex()
|
||||
.id(id)
|
||||
.gap_1p5()
|
||||
.w_full()
|
||||
.child(
|
||||
Icon::new(IconName::Code)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(&entry.symbol.name))
|
||||
.child(
|
||||
Label::new(symbol_location)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.when(entry.is_included, |el| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_end()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success),
|
||||
)
|
||||
.child(Label::new("Added").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use ui::{ListItem, prelude::*};
|
||||
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::{self, ContextStore};
|
||||
@@ -197,7 +197,7 @@ pub fn render_thread_context_entry(
|
||||
.gap_1p5()
|
||||
.max_w_72()
|
||||
.child(
|
||||
Icon::new(IconName::MessageCircle)
|
||||
Icon::new(IconName::MessageBubbles)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
@@ -1,20 +1,23 @@
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use futures::{self, future, Future, FutureExt};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::Buffer;
|
||||
use project::{ProjectPath, Worktree};
|
||||
use futures::future::join_all;
|
||||
use futures::{self, Future, FutureExt, future};
|
||||
use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity};
|
||||
use language::{Buffer, File};
|
||||
use project::{ProjectItem, ProjectPath, Worktree};
|
||||
use rope::Rope;
|
||||
use text::BufferId;
|
||||
use util::maybe;
|
||||
use text::{Anchor, BufferId, OffsetRangeExt};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSnapshot, DirectoryContext,
|
||||
FetchedUrlContext, FileContext, ThreadContext,
|
||||
AssistantContext, ContextBuffer, ContextId, ContextSymbol, ContextSymbolId, DirectoryContext,
|
||||
FetchedUrlContext, FileContext, SymbolContext, ThreadContext,
|
||||
};
|
||||
use crate::context_strip::SuggestedContext;
|
||||
use crate::thread::{Thread, ThreadId};
|
||||
@@ -22,37 +25,48 @@ use crate::thread::{Thread, ThreadId};
|
||||
pub struct ContextStore {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context: Vec<AssistantContext>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
// TODO: If an EntityId is used for all context types (like BufferId), can remove ContextId.
|
||||
next_context_id: ContextId,
|
||||
files: BTreeMap<BufferId, ContextId>,
|
||||
directories: HashMap<PathBuf, ContextId>,
|
||||
symbols: HashMap<ContextSymbolId, ContextId>,
|
||||
symbol_buffers: HashMap<ContextSymbolId, Entity<Buffer>>,
|
||||
symbols_by_path: HashMap<ProjectPath, Vec<ContextSymbolId>>,
|
||||
threads: HashMap<ThreadId, ContextId>,
|
||||
thread_summary_tasks: Vec<Task<()>>,
|
||||
fetched_urls: HashMap<String, ContextId>,
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(workspace: WeakEntity<Workspace>) -> Self {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
thread_store: Option<WeakEntity<ThreadStore>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
thread_store,
|
||||
context: Vec::new(),
|
||||
next_context_id: ContextId(0),
|
||||
files: BTreeMap::default(),
|
||||
directories: HashMap::default(),
|
||||
symbols: HashMap::default(),
|
||||
symbol_buffers: HashMap::default(),
|
||||
symbols_by_path: HashMap::default(),
|
||||
threads: HashMap::default(),
|
||||
thread_summary_tasks: Vec::new(),
|
||||
fetched_urls: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = ContextSnapshot> + 'a {
|
||||
self.context()
|
||||
.iter()
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &Vec<AssistantContext> {
|
||||
&self.context
|
||||
}
|
||||
|
||||
pub fn context_for_id(&self, id: ContextId) -> Option<&AssistantContext> {
|
||||
self.context().iter().find(|context| context.id() == id)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.context.clear();
|
||||
self.files.clear();
|
||||
@@ -81,14 +95,14 @@ impl ContextStore {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?;
|
||||
|
||||
let buffer_entity = open_buffer_task.await?;
|
||||
let buffer_id = this.update(cx, |_, cx| buffer_entity.read(cx).remote_id())?;
|
||||
let buffer = open_buffer_task.await?;
|
||||
let buffer_id = this.update(cx, |_, cx| buffer.read(cx).remote_id())?;
|
||||
|
||||
let already_included = this.update(cx, |this, _cx| {
|
||||
let already_included = this.update(cx, |this, cx| {
|
||||
match this.will_include_buffer(buffer_id, &project_path.path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
this.remove_context(context_id);
|
||||
this.remove_context(context_id, cx);
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -101,20 +115,13 @@ impl ContextStore {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
let (buffer_info, text_task) = this.update(cx, |_, cx| {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
collect_buffer_info_and_text(
|
||||
project_path.path.clone(),
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
)
|
||||
})?;
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.insert_file(make_context_buffer(buffer_info, text));
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_file(make_context_buffer(buffer_info, text), cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -123,38 +130,29 @@ impl ContextStore {
|
||||
|
||||
pub fn add_file_from_buffer(
|
||||
&mut self,
|
||||
buffer_entity: Entity<Buffer>,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
let (buffer_info, text_task) = this.update(cx, |_, cx| {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let Some(file) = buffer.file() else {
|
||||
return Err(anyhow!("Buffer has no path."));
|
||||
};
|
||||
Ok(collect_buffer_info_and_text(
|
||||
file.path().clone(),
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
))
|
||||
})??;
|
||||
let (buffer_info, text_task) =
|
||||
this.update(cx, |_, cx| collect_buffer_info_and_text(buffer, None, cx))??;
|
||||
|
||||
let text = text_task.await;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.insert_file(make_context_buffer(buffer_info, text))
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_file(make_context_buffer(buffer_info, text), cx)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_file(&mut self, context_buffer: ContextBuffer) {
|
||||
fn insert_file(&mut self, context_buffer: ContextBuffer, cx: &mut Context<Self>) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.files.insert(context_buffer.id, id);
|
||||
self.context
|
||||
.push(AssistantContext::File(FileContext { id, context_buffer }));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_directory(
|
||||
@@ -171,14 +169,15 @@ impl ContextStore {
|
||||
return Task::ready(Err(anyhow!("failed to read project")));
|
||||
};
|
||||
|
||||
let already_included = if let Some(context_id) = self.includes_directory(&project_path.path)
|
||||
{
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id);
|
||||
let already_included = match self.includes_directory(&project_path.path) {
|
||||
Some(FileInclusion::Direct(context_id)) => {
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id, cx);
|
||||
}
|
||||
true
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
Some(FileInclusion::InDirectory(_)) => true,
|
||||
None => false,
|
||||
};
|
||||
if already_included {
|
||||
return Task::ready(Ok(()));
|
||||
@@ -214,16 +213,11 @@ impl ContextStore {
|
||||
let mut buffer_infos = Vec::new();
|
||||
let mut text_tasks = Vec::new();
|
||||
this.update(cx, |_, cx| {
|
||||
for (path, buffer_entity) in files.into_iter().zip(buffers) {
|
||||
// Skip all binary files and other non-UTF8 files
|
||||
if let Ok(buffer_entity) = buffer_entity {
|
||||
let buffer = buffer_entity.read(cx);
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
path,
|
||||
buffer_entity,
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
);
|
||||
// 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()
|
||||
{
|
||||
buffer_infos.push(buffer_info);
|
||||
text_tasks.push(text_task);
|
||||
}
|
||||
@@ -239,27 +233,112 @@ impl ContextStore {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if context_buffers.is_empty() {
|
||||
bail!("No text files found in {}", &project_path.path.display());
|
||||
return Err(anyhow!(
|
||||
"No text files found in {}",
|
||||
&project_path.path.display()
|
||||
));
|
||||
}
|
||||
|
||||
this.update(cx, |this, _| {
|
||||
this.insert_directory(&project_path.path, context_buffers);
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_directory(project_path, context_buffers, cx);
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_directory(&mut self, path: &Path, context_buffers: Vec<ContextBuffer>) {
|
||||
fn insert_directory(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
context_buffers: Vec<ContextBuffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.directories.insert(path.to_path_buf(), id);
|
||||
self.directories.insert(project_path.path.to_path_buf(), id);
|
||||
|
||||
self.context
|
||||
.push(AssistantContext::Directory(DirectoryContext::new(
|
||||
.push(AssistantContext::Directory(DirectoryContext {
|
||||
id,
|
||||
path,
|
||||
project_path,
|
||||
context_buffers,
|
||||
)));
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_symbol(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
symbol_name: SharedString,
|
||||
symbol_range: Range<Anchor>,
|
||||
symbol_enclosing_range: Range<Anchor>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
let buffer_ref = buffer.read(cx);
|
||||
let Some(project_path) = buffer_ref.project_path(cx) else {
|
||||
return Task::ready(Err(anyhow!("buffer has no path")));
|
||||
};
|
||||
|
||||
if let Some(symbols_for_path) = self.symbols_by_path.get(&project_path) {
|
||||
let mut matching_symbol_id = None;
|
||||
for symbol in symbols_for_path {
|
||||
if &symbol.name == &symbol_name {
|
||||
let snapshot = buffer_ref.snapshot();
|
||||
if symbol.range.to_offset(&snapshot) == symbol_range.to_offset(&snapshot) {
|
||||
matching_symbol_id = self.symbols.get(symbol).cloned();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(id) = matching_symbol_id {
|
||||
if remove_if_exists {
|
||||
self.remove_context(id, cx);
|
||||
}
|
||||
return Task::ready(Ok(false));
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let content = collect_content_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_symbol(
|
||||
make_context_symbol(
|
||||
buffer_info,
|
||||
project_path,
|
||||
symbol_name,
|
||||
symbol_range,
|
||||
symbol_enclosing_range,
|
||||
content,
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
anyhow::Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_symbol(&mut self, context_symbol: ContextSymbol, cx: &mut Context<Self>) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
self.symbols.insert(context_symbol.id.clone(), id);
|
||||
self.symbols_by_path
|
||||
.entry(context_symbol.id.path.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(context_symbol.id.clone());
|
||||
self.symbol_buffers
|
||||
.insert(context_symbol.id.clone(), context_symbol.buffer.clone());
|
||||
self.context.push(AssistantContext::Symbol(SymbolContext {
|
||||
id,
|
||||
context_symbol,
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_thread(
|
||||
@@ -270,29 +349,70 @@ impl ContextStore {
|
||||
) {
|
||||
if let Some(context_id) = self.includes_thread(&thread.read(cx).id()) {
|
||||
if remove_if_exists {
|
||||
self.remove_context(context_id);
|
||||
self.remove_context(context_id, cx);
|
||||
}
|
||||
} else {
|
||||
self.insert_thread(thread, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &App) {
|
||||
pub fn wait_for_summaries(&mut self, cx: &App) -> Task<()> {
|
||||
let tasks = std::mem::take(&mut self.thread_summary_tasks);
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
join_all(tasks).await;
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
|
||||
if let Some(summary_task) =
|
||||
thread.update(cx, |thread, cx| thread.generate_detailed_summary(cx))
|
||||
{
|
||||
let thread = thread.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
|
||||
self.thread_summary_tasks.push(cx.spawn(async move |_, cx| {
|
||||
summary_task.await;
|
||||
|
||||
if let Some(thread_store) = thread_store {
|
||||
// Save thread so its summary can be reused later
|
||||
let save_task = thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx));
|
||||
|
||||
if let Some(save_task) = save_task.ok() {
|
||||
save_task.await.log_err();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let id = self.next_context_id.post_inc();
|
||||
let text = thread.read(cx).text().into();
|
||||
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
|
||||
self.threads.insert(thread.read(cx).id().clone(), id);
|
||||
self.context
|
||||
.push(AssistantContext::Thread(ThreadContext { id, thread, text }));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
|
||||
pub fn add_fetched_url(
|
||||
&mut self,
|
||||
url: String,
|
||||
text: impl Into<SharedString>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
if self.includes_url(&url).is_none() {
|
||||
self.insert_fetched_url(url, text);
|
||||
self.insert_fetched_url(url, text, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_fetched_url(&mut self, url: String, text: impl Into<SharedString>) {
|
||||
fn insert_fetched_url(
|
||||
&mut self,
|
||||
url: String,
|
||||
text: impl Into<SharedString>,
|
||||
cx: &mut Context<ContextStore>,
|
||||
) {
|
||||
let id = self.next_context_id.post_inc();
|
||||
|
||||
self.fetched_urls.insert(url.clone(), id);
|
||||
@@ -302,6 +422,7 @@ impl ContextStore {
|
||||
url: url.into(),
|
||||
text: text.into(),
|
||||
}));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn accept_suggested_context(
|
||||
@@ -328,7 +449,7 @@ impl ContextStore {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
pub fn remove_context(&mut self, id: ContextId) {
|
||||
pub fn remove_context(&mut self, id: ContextId, cx: &mut Context<Self>) {
|
||||
let Some(ix) = self.context.iter().position(|context| context.id() == id) else {
|
||||
return;
|
||||
};
|
||||
@@ -340,6 +461,19 @@ impl ContextStore {
|
||||
AssistantContext::Directory(_) => {
|
||||
self.directories.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::Symbol(symbol) => {
|
||||
if let Some(symbols_in_path) =
|
||||
self.symbols_by_path.get_mut(&symbol.context_symbol.id.path)
|
||||
{
|
||||
symbols_in_path.retain(|s| {
|
||||
self.symbols
|
||||
.get(s)
|
||||
.map_or(false, |context_id| *context_id != id)
|
||||
});
|
||||
}
|
||||
self.symbol_buffers.remove(&symbol.context_symbol.id);
|
||||
self.symbols.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
AssistantContext::FetchedUrl(_) => {
|
||||
self.fetched_urls.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
@@ -347,6 +481,8 @@ impl ContextStore {
|
||||
self.threads.retain(|_, context_id| *context_id != id);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Returns whether the buffer is already included directly in the context, or if it will be
|
||||
@@ -367,7 +503,7 @@ impl ContextStore {
|
||||
let found_file_context = self.context.iter().find(|context| match &context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let buffer = file_context.context_buffer.buffer.read(cx);
|
||||
if let Some(file_path) = buffer_path_log_err(buffer) {
|
||||
if let Some(file_path) = buffer_path_log_err(buffer, cx) {
|
||||
*file_path == *path
|
||||
} else {
|
||||
false
|
||||
@@ -399,8 +535,24 @@ impl ContextStore {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<ContextId> {
|
||||
self.directories.get(path).copied()
|
||||
pub fn includes_directory(&self, path: &Path) -> Option<FileInclusion> {
|
||||
if let Some(context_id) = self.directories.get(path) {
|
||||
return Some(FileInclusion::Direct(*context_id));
|
||||
}
|
||||
|
||||
self.will_include_file_path_via_directory(path)
|
||||
}
|
||||
|
||||
pub fn included_symbol(&self, symbol_id: &ContextSymbolId) -> Option<ContextId> {
|
||||
self.symbols.get(symbol_id).copied()
|
||||
}
|
||||
|
||||
pub fn included_symbols_by_path(&self) -> &HashMap<ProjectPath, Vec<ContextSymbolId>> {
|
||||
&self.symbols_by_path
|
||||
}
|
||||
|
||||
pub fn buffer_for_symbol(&self, symbol_id: &ContextSymbolId) -> Option<Entity<Buffer>> {
|
||||
self.symbol_buffers.get(symbol_id).cloned()
|
||||
}
|
||||
|
||||
pub fn includes_thread(&self, thread_id: &ThreadId) -> Option<ContextId> {
|
||||
@@ -428,9 +580,10 @@ impl ContextStore {
|
||||
.filter_map(|context| match context {
|
||||
AssistantContext::File(file) => {
|
||||
let buffer = file.context_buffer.buffer.read(cx);
|
||||
buffer_path_log_err(buffer).map(|p| p.to_path_buf())
|
||||
buffer_path_log_err(buffer, cx).map(|p| p.to_path_buf())
|
||||
}
|
||||
AssistantContext::Directory(_)
|
||||
| AssistantContext::Symbol(_)
|
||||
| AssistantContext::FetchedUrl(_)
|
||||
| AssistantContext::Thread(_) => None,
|
||||
})
|
||||
@@ -449,40 +602,75 @@ pub enum FileInclusion {
|
||||
|
||||
// ContextBuffer without text.
|
||||
struct BufferInfo {
|
||||
buffer_entity: Entity<Buffer>,
|
||||
id: BufferId,
|
||||
buffer: Entity<Buffer>,
|
||||
file: Arc<dyn File>,
|
||||
version: clock::Global,
|
||||
}
|
||||
|
||||
fn make_context_buffer(info: BufferInfo, text: SharedString) -> ContextBuffer {
|
||||
ContextBuffer {
|
||||
id: info.id,
|
||||
buffer: info.buffer_entity,
|
||||
buffer: info.buffer,
|
||||
file: info.file,
|
||||
version: info.version,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_buffer_info_and_text(
|
||||
path: Arc<Path>,
|
||||
buffer_entity: Entity<Buffer>,
|
||||
buffer: &Buffer,
|
||||
cx: AsyncApp,
|
||||
) -> (BufferInfo, Task<SharedString>) {
|
||||
let buffer_info = BufferInfo {
|
||||
id: buffer.remote_id(),
|
||||
buffer_entity,
|
||||
version: buffer.version(),
|
||||
};
|
||||
// Important to collect version at the same time as content so that staleness logic is correct.
|
||||
let content = buffer.as_rope().clone();
|
||||
let text_task = cx.background_spawn(async move { to_fenced_codeblock(&path, content) });
|
||||
(buffer_info, text_task)
|
||||
fn make_context_symbol(
|
||||
info: BufferInfo,
|
||||
path: ProjectPath,
|
||||
name: SharedString,
|
||||
range: Range<Anchor>,
|
||||
enclosing_range: Range<Anchor>,
|
||||
text: SharedString,
|
||||
) -> ContextSymbol {
|
||||
ContextSymbol {
|
||||
id: ContextSymbolId { name, range, path },
|
||||
buffer_version: info.version,
|
||||
enclosing_range,
|
||||
buffer: info.buffer,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_path_log_err(buffer: &Buffer) -> Option<Arc<Path>> {
|
||||
fn collect_buffer_info_and_text(
|
||||
buffer: Entity<Buffer>,
|
||||
range: Option<Range<Anchor>>,
|
||||
cx: &App,
|
||||
) -> Result<(BufferInfo, Task<SharedString>)> {
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
pub fn buffer_path_log_err(buffer: &Buffer, cx: &App) -> Option<Arc<Path>> {
|
||||
if let Some(file) = buffer.file() {
|
||||
Some(file.path().clone())
|
||||
let mut path = file.path().clone();
|
||||
if path.as_os_str().is_empty() {
|
||||
path = file.full_path(cx).into();
|
||||
}
|
||||
Some(path)
|
||||
} else {
|
||||
log::error!("Buffer that had a path unexpectedly no longer has a path.");
|
||||
None
|
||||
@@ -547,7 +735,7 @@ pub fn refresh_context_store_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
changed_buffers: &HashSet<Entity<Buffer>>,
|
||||
cx: &App,
|
||||
) -> impl Future<Output = Vec<ContextId>> {
|
||||
) -> impl Future<Output = Vec<ContextId>> + use<> {
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for context in &context_store.read(cx).context {
|
||||
@@ -568,8 +756,9 @@ pub fn refresh_context_store_text(
|
||||
|| changed_buffers.iter().any(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
buffer_path_log_err(&buffer)
|
||||
.map_or(false, |path| path.starts_with(&directory_context.path))
|
||||
buffer_path_log_err(&buffer, cx).map_or(false, |path| {
|
||||
path.starts_with(&directory_context.project_path.path)
|
||||
})
|
||||
});
|
||||
|
||||
if should_refresh {
|
||||
@@ -577,6 +766,14 @@ pub fn refresh_context_store_text(
|
||||
return refresh_directory_text(context_store, directory_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Symbol(symbol_context) => {
|
||||
if changed_buffers.is_empty()
|
||||
|| changed_buffers.contains(&symbol_context.context_symbol.buffer)
|
||||
{
|
||||
let context_store = context_store.clone();
|
||||
return refresh_symbol_text(context_store, symbol_context, cx);
|
||||
}
|
||||
}
|
||||
AssistantContext::Thread(thread_context) => {
|
||||
if changed_buffers.is_empty() {
|
||||
let context_store = context_store.clone();
|
||||
@@ -647,19 +844,45 @@ fn refresh_directory_text(
|
||||
|
||||
let context_buffers = future::join_all(futures);
|
||||
|
||||
let id = directory_context.snapshot.id;
|
||||
let path = directory_context.path.clone();
|
||||
let id = directory_context.id;
|
||||
let project_path = directory_context.project_path.clone();
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let context_buffers = context_buffers.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_directory_context = DirectoryContext::new(id, &path, context_buffers);
|
||||
let new_directory_context = DirectoryContext {
|
||||
id,
|
||||
project_path,
|
||||
context_buffers,
|
||||
};
|
||||
context_store.replace_context(AssistantContext::Directory(new_directory_context));
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
}
|
||||
|
||||
fn refresh_symbol_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
symbol_context: &SymbolContext,
|
||||
cx: &App,
|
||||
) -> Option<Task<()>> {
|
||||
let id = symbol_context.id;
|
||||
let task = refresh_context_symbol(&symbol_context.context_symbol, cx);
|
||||
if let Some(task) = task {
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let context_symbol = task.await;
|
||||
context_store
|
||||
.update(cx, |context_store, _| {
|
||||
let new_symbol_context = SymbolContext { id, context_symbol };
|
||||
context_store.replace_context(AssistantContext::Symbol(new_symbol_context));
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_thread_text(
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_context: &ThreadContext,
|
||||
@@ -670,7 +893,7 @@ fn refresh_thread_text(
|
||||
cx.spawn(async move |cx| {
|
||||
context_store
|
||||
.update(cx, |context_store, cx| {
|
||||
let text = thread.read(cx).text().into();
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
context_store.replace_context(AssistantContext::Thread(ThreadContext {
|
||||
id,
|
||||
thread,
|
||||
@@ -684,18 +907,44 @@ fn refresh_thread_text(
|
||||
fn refresh_context_buffer(
|
||||
context_buffer: &ContextBuffer,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = ContextBuffer>> {
|
||||
) -> Option<impl Future<Output = ContextBuffer> + use<>> {
|
||||
let buffer = context_buffer.buffer.read(cx);
|
||||
let path = buffer_path_log_err(buffer)?;
|
||||
if buffer.version.changed_since(&context_buffer.version) {
|
||||
let (buffer_info, text_task) = collect_buffer_info_and_text(
|
||||
path,
|
||||
context_buffer.buffer.clone(),
|
||||
buffer,
|
||||
cx.to_async(),
|
||||
);
|
||||
let (buffer_info, text_task) =
|
||||
collect_buffer_info_and_text(context_buffer.buffer.clone(), None, cx).log_err()?;
|
||||
Some(text_task.map(move |text| make_context_buffer(buffer_info, text)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_context_symbol(
|
||||
context_symbol: &ContextSymbol,
|
||||
cx: &App,
|
||||
) -> Option<impl Future<Output = ContextSymbol> + use<>> {
|
||||
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(
|
||||
context_symbol.buffer.clone(),
|
||||
Some(context_symbol.enclosing_range.clone()),
|
||||
cx,
|
||||
)
|
||||
.log_err()?;
|
||||
let name = context_symbol.id.name.clone();
|
||||
let range = context_symbol.id.range.clone();
|
||||
let enclosing_range = context_symbol.enclosing_range.clone();
|
||||
Some(text_task.map(move |text| {
|
||||
make_context_symbol(
|
||||
buffer_info,
|
||||
project_path,
|
||||
name,
|
||||
range,
|
||||
enclosing_range,
|
||||
text,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -4,20 +4,20 @@ use collections::HashSet;
|
||||
use editor::Editor;
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{
|
||||
App, Bounds, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription,
|
||||
WeakEntity,
|
||||
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Subscription, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||
use ui::{KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context::{ContextId, ContextKind};
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::ContextPill;
|
||||
use crate::ui::{AddedContext, ContextPill};
|
||||
use crate::{
|
||||
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
|
||||
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
|
||||
@@ -59,6 +59,7 @@ impl ContextStrip {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
|
||||
cx.on_focus(&focus_handle, window, Self::handle_focus),
|
||||
cx.on_blur(&focus_handle, window, Self::handle_blur),
|
||||
@@ -92,12 +93,12 @@ impl ContextStrip {
|
||||
let active_buffer_entity = editor.buffer().read(cx).as_singleton()?;
|
||||
let active_buffer = active_buffer_entity.read(cx);
|
||||
|
||||
let path = active_buffer.file()?.path();
|
||||
let path = active_buffer.file()?.full_path(cx);
|
||||
|
||||
if self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.will_include_buffer(active_buffer.remote_id(), path)
|
||||
.will_include_buffer(active_buffer.remote_id(), &path)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
@@ -108,7 +109,7 @@ impl ContextStrip {
|
||||
None => path.to_string_lossy().into_owned().into(),
|
||||
};
|
||||
|
||||
let icon_path = FileIcons::get_icon(path, cx);
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
|
||||
Some(SuggestedContext::File {
|
||||
name,
|
||||
@@ -239,11 +240,7 @@ impl ContextStrip {
|
||||
let eraser = if bounds.len() < 3 { 0 } else { 1 };
|
||||
let pills = &bounds[1..bounds.len() - eraser];
|
||||
|
||||
if pills.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(pills)
|
||||
}
|
||||
if pills.is_empty() { None } else { Some(pills) }
|
||||
}
|
||||
|
||||
fn last_pill_index(&self) -> Option<usize> {
|
||||
@@ -277,6 +274,14 @@ impl ContextStrip {
|
||||
best.map(|(index, _, _)| index)
|
||||
}
|
||||
|
||||
fn open_context(&mut self, id: ContextId, window: &mut Window, cx: &mut App) {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
crate::active_thread::open_context(id, self.context_store.clone(), workspace, window, cx);
|
||||
}
|
||||
|
||||
fn remove_focused_context(
|
||||
&mut self,
|
||||
_: &RemoveFocusedContext,
|
||||
@@ -286,9 +291,9 @@ impl ContextStrip {
|
||||
if let Some(index) = self.focused_index {
|
||||
let mut is_empty = false;
|
||||
|
||||
self.context_store.update(cx, |this, _cx| {
|
||||
self.context_store.update(cx, |this, cx| {
|
||||
if let Some(item) = this.context().get(index) {
|
||||
this.remove_context(item.id());
|
||||
this.remove_context(item.id(), cx);
|
||||
}
|
||||
|
||||
is_empty = this.context().is_empty();
|
||||
@@ -359,19 +364,19 @@ impl Focusable for ContextStrip {
|
||||
impl Render for ContextStrip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let context_store = self.context_store.read(cx);
|
||||
let context = context_store
|
||||
.context()
|
||||
.iter()
|
||||
.flat_map(|context| context.snapshot(cx))
|
||||
.collect::<Vec<_>>();
|
||||
let context = context_store.context();
|
||||
let context_picker = self.context_picker.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let suggested_context = self.suggested_context(cx);
|
||||
|
||||
let dupe_names = context
|
||||
let added_contexts = context
|
||||
.iter()
|
||||
.map(|context| context.name.clone())
|
||||
.map(|c| AddedContext::new(c, cx))
|
||||
.collect::<Vec<_>>();
|
||||
let dupe_names = added_contexts
|
||||
.iter()
|
||||
.map(|c| c.name.clone())
|
||||
.sorted()
|
||||
.tuple_windows()
|
||||
.filter(|(a, b)| a == b)
|
||||
@@ -457,27 +462,39 @@ impl Render for ContextStrip {
|
||||
)
|
||||
}
|
||||
})
|
||||
.children(context.iter().enumerate().map(|(i, context)| {
|
||||
ContextPill::added(
|
||||
context.clone(),
|
||||
dupe_names.contains(&context.name),
|
||||
self.focused_index == Some(i),
|
||||
Some({
|
||||
let id = context.id;
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||
context_store.update(cx, |this, _cx| {
|
||||
this.remove_context(id);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
.children(
|
||||
added_contexts
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, added_context)| {
|
||||
let name = added_context.name.clone();
|
||||
let id = added_context.id;
|
||||
ContextPill::added(
|
||||
added_context,
|
||||
dupe_names.contains(&name),
|
||||
self.focused_index == Some(i),
|
||||
Some({
|
||||
let context_store = self.context_store.clone();
|
||||
Rc::new(cx.listener(move |_this, _event, _window, cx| {
|
||||
context_store.update(cx, |this, cx| {
|
||||
this.remove_context(id, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.on_click({
|
||||
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
|
||||
if event.down.click_count > 1 {
|
||||
this.open_context(id, window, cx);
|
||||
} else {
|
||||
this.focused_index = Some(i);
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
}),
|
||||
)
|
||||
.on_click(Rc::new(cx.listener(move |this, _, _window, cx| {
|
||||
this.focused_index = Some(i);
|
||||
cx.notify();
|
||||
})))
|
||||
}))
|
||||
)
|
||||
.when_some(suggested_context, |el, suggested| {
|
||||
el.child(
|
||||
ContextPill::suggested(
|
||||
@@ -1,9 +1,10 @@
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{prelude::*, Entity};
|
||||
use gpui::{Entity, prelude::*};
|
||||
|
||||
use crate::thread_store::{SerializedThreadMetadata, ThreadStore};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HistoryEntry {
|
||||
Thread(SerializedThreadMetadata),
|
||||
Context(SavedContextMetadata),
|
||||
@@ -21,28 +22,35 @@ impl HistoryEntry {
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context_editor::ContextStore>,
|
||||
_cx: &mut Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread_store, |_, _, cx| cx.notify()),
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of history entries.
|
||||
pub fn entry_count(&self, cx: &mut Context<Self>) -> usize {
|
||||
self.entries(cx).len()
|
||||
}
|
||||
|
||||
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
let mut history_entries = Vec::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
for thread in self.thread_store.update(cx, |this, _cx| this.threads()) {
|
||||
history_entries.push(HistoryEntry::Thread(thread));
|
||||
}
|
||||
@@ -7,24 +7,24 @@ use std::sync::Arc;
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
actions::SelectAll,
|
||||
display_map::{
|
||||
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
ToDisplayPoint,
|
||||
},
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
|
||||
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
point, App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task,
|
||||
UpdateGlobal, WeakEntity, Window,
|
||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity, Window, point,
|
||||
};
|
||||
use language::{Buffer, Point, Selection, TransactionId};
|
||||
use language_model::{report_assistant_event, LanguageModelRegistry};
|
||||
use language_model::{LanguageModelRegistry, report_assistant_event};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::LspAction;
|
||||
@@ -32,20 +32,20 @@ use project::{CodeAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use ui::prelude::*;
|
||||
use util::RangeExt;
|
||||
use util::ResultExt;
|
||||
use workspace::{dock::Panel, ShowConfiguration};
|
||||
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
|
||||
use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
|
||||
use crate::terminal_inline_assistant::TerminalInlineAssistant;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::AssistantPanel;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -239,8 +239,8 @@ impl InlineAssistant {
|
||||
|
||||
let is_authenticated = || {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
.inline_assistant_model()
|
||||
.map_or(false, |model| model.provider.is_authenticated(cx))
|
||||
};
|
||||
|
||||
let thread_store = workspace
|
||||
@@ -279,8 +279,8 @@ impl InlineAssistant {
|
||||
cx.spawn_in(window, async move |_workspace, cx| {
|
||||
let Some(task) = cx.update(|_, cx| {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map_or(None, |provider| Some(provider.authenticate(cx)))
|
||||
.inline_assistant_model()
|
||||
.map_or(None, |model| Some(model.provider.authenticate(cx)))
|
||||
})?
|
||||
else {
|
||||
let answer = cx
|
||||
@@ -295,7 +295,7 @@ impl InlineAssistant {
|
||||
if let Some(answer) = answer {
|
||||
if answer == 0 {
|
||||
cx.update(|window, cx| {
|
||||
window.dispatch_action(Box::new(ShowConfiguration), cx)
|
||||
window.dispatch_action(Box::new(OpenConfiguration), cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -401,14 +401,14 @@ impl InlineAssistant {
|
||||
|
||||
codegen_ranges.push(anchor_range);
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
||||
self.telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Invoked,
|
||||
message_id: None,
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
model: model.model.telemetry_id(),
|
||||
model_provider: model.provider.id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: buffer.language().map(|language| language.name().to_proto()),
|
||||
@@ -424,7 +424,8 @@ impl InlineAssistant {
|
||||
let mut assist_to_focus = None;
|
||||
for range in codegen_ranges {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
editor.read(cx).buffer().clone(),
|
||||
@@ -536,7 +537,8 @@ impl InlineAssistant {
|
||||
range.end = range.end.bias_right(&snapshot);
|
||||
}
|
||||
|
||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
|
||||
let codegen = cx.new(|cx| {
|
||||
BufferCodegen::new(
|
||||
@@ -619,14 +621,14 @@ impl InlineAssistant {
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Above(range.start),
|
||||
height: prompt_editor_height,
|
||||
height: Some(prompt_editor_height),
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Below(range.end),
|
||||
height: 0,
|
||||
height: None,
|
||||
render: Arc::new(|cx| {
|
||||
v_flex()
|
||||
.h_full()
|
||||
@@ -974,7 +976,7 @@ impl InlineAssistant {
|
||||
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||
let message_id = active_alternative.read(cx).message_id.clone();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() {
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let snapshot = multibuffer.snapshot(cx);
|
||||
@@ -994,15 +996,15 @@ impl InlineAssistant {
|
||||
} else {
|
||||
AssistantPhase::Accepted
|
||||
},
|
||||
model: model.telemetry_id(),
|
||||
model_provider: model.provider_id().to_string(),
|
||||
model: model.model.telemetry_id(),
|
||||
model_provider: model.model.provider_id().to_string(),
|
||||
response_latency: None,
|
||||
error_message: None,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
},
|
||||
Some(self.telemetry.clone()),
|
||||
cx.http_client(),
|
||||
model.api_key(cx),
|
||||
model.model.api_key(cx),
|
||||
cx.background_executor(),
|
||||
);
|
||||
}
|
||||
@@ -1363,10 +1365,7 @@ impl InlineAssistant {
|
||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer.push_excerpts(
|
||||
old_buffer.clone(),
|
||||
Some(ExcerptRange {
|
||||
context: buffer_start..buffer_end,
|
||||
primary: None,
|
||||
}),
|
||||
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -1393,7 +1392,7 @@ impl InlineAssistant {
|
||||
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
|
||||
new_blocks.push(BlockProperties {
|
||||
placement: BlockPlacement::Above(new_row),
|
||||
height,
|
||||
height: Some(height),
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |cx| {
|
||||
div()
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
|
||||
use crate::buffer_codegen::BufferCodegen;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -10,14 +10,14 @@ use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp},
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, GutterDimensions, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedPro};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window,
|
||||
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
@@ -28,7 +28,7 @@ use std::sync::Arc;
|
||||
use theme::ThemeSettings;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
prelude::*, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip,
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
@@ -455,47 +455,55 @@ impl<T: 'static> PromptEditor<T> {
|
||||
|
||||
match codegen_status {
|
||||
CodegenStatus::Idle => {
|
||||
vec![Button::new("start", mode.start_label())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)))
|
||||
.into_any_element()]
|
||||
vec![
|
||||
Button::new("start", mode.start_label())
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Return)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(
|
||||
cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
]
|
||||
}
|
||||
CodegenStatus::Pending => vec![IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element()],
|
||||
CodegenStatus::Pending => vec![
|
||||
IconButton::new("stop", IconName::Stop)
|
||||
.icon_color(Color::Error)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_interrupt(),
|
||||
Some(&menu::Cancel),
|
||||
"Changes won't be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
|
||||
.into_any_element(),
|
||||
],
|
||||
CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
let has_error = matches!(codegen_status, CodegenStatus::Error(_));
|
||||
if has_error || self.edited_since_done {
|
||||
vec![IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element()]
|
||||
vec![
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
mode.tooltip_restart(),
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
let accept = IconButton::new("accept", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
@@ -574,7 +582,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let default_model = model_registry.active_model();
|
||||
let default_model = model_registry.default_model().map(|default| default.model);
|
||||
let alternative_models = model_registry.inline_alternative_models();
|
||||
|
||||
let get_model_name = |index: usize| -> String {
|
||||
@@ -882,6 +890,7 @@ impl PromptEditor<BufferCodegen> {
|
||||
fs,
|
||||
model_selector_menu_handle,
|
||||
prompt_editor.focus_handle(cx),
|
||||
ModelType::InlineAssistant,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1034,6 +1043,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||
fs,
|
||||
model_selector_menu_handle.clone(),
|
||||
prompt_editor.focus_handle(cx),
|
||||
ModelType::InlineAssistant,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
874
crates/agent/src/message_editor.rs
Normal file
874
crates/agent/src/message_editor.rs
Normal file
@@ -0,0 +1,874 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::assistant_model_selector::ModelType;
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ButtonLike, Disclosure, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{ContextStore, refresh_context_store_text};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{RequestKind, Thread, TokenUsageRatio};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{
|
||||
AgentDiff, Chat, ChatMode, NewThread, OpenAgentDiff, RemoveAllContext, ThreadEvent,
|
||||
ToggleContextPicker, ToggleProfileSelector,
|
||||
};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Entity<Thread>,
|
||||
editor: Entity<Editor>,
|
||||
#[allow(dead_code)]
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
context_store: Entity<ContextStore>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
inline_context_picker: Entity<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
edits_expanded: bool,
|
||||
waiting_for_summaries_to_send: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
min_entries_visible: 12,
|
||||
max_entries_visible: 12,
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
Some(thread_store.clone()),
|
||||
editor_entity,
|
||||
))));
|
||||
});
|
||||
|
||||
let inline_context_picker = cx.new(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::Close,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let context_strip = cx.new(|cx| {
|
||||
ContextStrip::new(
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::File,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(
|
||||
&inline_context_picker,
|
||||
window,
|
||||
Self::handle_inline_context_picker_event,
|
||||
),
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||
];
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
thread,
|
||||
workspace,
|
||||
context_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelType::Default,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
edits_expanded: false,
|
||||
waiting_for_summaries_to_send: false,
|
||||
profile_selector: cx
|
||||
.new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_context_picker(
|
||||
&mut self,
|
||||
_: &ToggleContextPicker,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
pub fn remove_all_context(
|
||||
&mut self,
|
||||
_: &RemoveAllContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.is_editor_empty(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
}
|
||||
|
||||
fn is_editor_empty(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).text(cx).is_empty()
|
||||
}
|
||||
|
||||
fn is_model_selected(&self, cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if provider.must_accept_terms(cx) {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let user_message = self.editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.clear(window, cx);
|
||||
text
|
||||
});
|
||||
|
||||
let refresh_task =
|
||||
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
|
||||
|
||||
let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
|
||||
|
||||
let thread = self.thread.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let checkpoint = checkpoint.await.ok();
|
||||
refresh_task.await;
|
||||
let (system_prompt_context, load_error) = system_prompt_context_task.await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_system_prompt_context(system_prompt_context);
|
||||
if let Some(load_error) = load_error {
|
||||
cx.emit(ThreadEvent::ShowError(load_error));
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
let context = context_store.read(cx).context().clone();
|
||||
thread.insert_user_message(user_message, context, checkpoint, cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(wait_for_summaries) = context_store
|
||||
.update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
|
||||
.log_err()
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = true;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
|
||||
wait_for_summaries.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.waiting_for_summaries_to_send = false;
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
// Send to model after summaries are done
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_to_model(model, request_kind, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_inline_context_picker_event(
|
||||
&mut self,
|
||||
_inline_context_picker: &Entity<ContextPicker>,
|
||||
_event: &DismissEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
window.focus(&editor_focus_handle);
|
||||
}
|
||||
|
||||
fn handle_context_strip_event(
|
||||
&mut self,
|
||||
_context_strip: &Entity<ContextStrip>,
|
||||
event: &ContextStripEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ContextStripEvent::PickerDismissed
|
||||
| ContextStripEvent::BlurredEmpty
|
||||
| ContextStripEvent::BlurredDown => {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
window.focus(&editor_focus_handle);
|
||||
}
|
||||
ContextStripEvent::BlurredUp => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.context_picker_menu_handle.is_deployed()
|
||||
|| self.inline_context_picker_menu_handle.is_deployed()
|
||||
{
|
||||
cx.propagate();
|
||||
} else {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
|
||||
fn handle_file_click(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Ok(diff) = AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
|
||||
{
|
||||
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
||||
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let inline_context_picker = self.inline_context_picker.clone();
|
||||
|
||||
let thread = self.thread.read(cx);
|
||||
let is_generating = thread.is_generating();
|
||||
let total_token_usage = thread.total_token_usage(cx);
|
||||
let is_model_selected = self.is_model_selected(cx);
|
||||
let is_editor_empty = self.is_editor_empty(cx);
|
||||
let needs_confirmation =
|
||||
thread.has_pending_tool_uses() && thread.tools_needing_confirmation().next().is_some();
|
||||
|
||||
let submit_label_color = if is_editor_empty {
|
||||
Color::Muted
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
|
||||
let vim_mode_enabled = VimModeSetting::get_global(cx).0;
|
||||
let platform = PlatformStyle::platform();
|
||||
let linux = platform == PlatformStyle::Linux;
|
||||
let windows = platform == PlatformStyle::Windows;
|
||||
let button_width = if linux || windows || vim_mode_enabled {
|
||||
px(82.)
|
||||
} else {
|
||||
px(64.)
|
||||
};
|
||||
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
let changed_buffers_count = changed_buffers.len();
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let border_color = cx.theme().colors().border;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.when(self.waiting_for_summaries_to_send, |parent| {
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.px_2()
|
||||
.py_2()
|
||||
.bg(editor_bg_color)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Summarizing context…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(is_generating, |parent| {
|
||||
let focus_handle = self.editor.focus_handle(cx).clone();
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.bg(editor_bg_color)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child({
|
||||
|
||||
|
||||
Label::new(if needs_confirmation {
|
||||
"Waiting for confirmation…"
|
||||
} else {
|
||||
"Generating…"
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
})
|
||||
.child(ui::Divider::vertical())
|
||||
.child(
|
||||
Button::new("cancel-generation", "Cancel")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(changed_buffers_count > 0, |parent| {
|
||||
parent.child(
|
||||
v_flex()
|
||||
.mx_2()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_b_0()
|
||||
.border_color(border_color)
|
||||
.rounded_t_md()
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
h_flex()
|
||||
.id("edits-container")
|
||||
.p_1p5()
|
||||
.justify_between()
|
||||
.when(self.edits_expanded, |this| {
|
||||
this.border_b_1().border_color(border_color)
|
||||
})
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.handle_review_click(window, cx)
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
"edits-disclosure",
|
||||
self.edits_expanded,
|
||||
)
|
||||
.on_click(
|
||||
cx.listener(|this, _ev, _window, cx| {
|
||||
this.edits_expanded = !this.edits_expanded;
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Edits")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new("•")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_buffers_count,
|
||||
if changed_buffers_count == 1 {
|
||||
"file"
|
||||
} else {
|
||||
"files"
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("review", "Review Changes")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&OpenAgentDiff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(12.))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.handle_review_click(window, cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(self.edits_expanded, |parent| {
|
||||
parent.child(
|
||||
v_flex().bg(cx.theme().colors().editor_background).children(
|
||||
changed_buffers.into_iter().enumerate().flat_map(
|
||||
|(index, (buffer, _diff))| {
|
||||
let file = buffer.read(cx).file()?;
|
||||
let path = file.path();
|
||||
|
||||
let parent_label = path.parent().and_then(|parent| {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
|
||||
if parent_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(format!(
|
||||
"{}{}",
|
||||
parent_str,
|
||||
std::path::MAIN_SEPARATOR_STR
|
||||
))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
let name_label = path.file_name().map(|name| {
|
||||
Label::new(name.to_string_lossy().to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.buffer_font(cx)
|
||||
});
|
||||
|
||||
let file_icon = FileIcons::get_icon(&path, cx)
|
||||
.map(Icon::from_path)
|
||||
.map(|icon| {
|
||||
icon.color(Color::Muted).size(IconSize::Small)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Icon::new(IconName::File)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
});
|
||||
|
||||
let element = div()
|
||||
.relative()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.when(index + 1 < changed_buffers_count, |parent| {
|
||||
parent.border_color(border_color).border_b_1()
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("file-container", index))
|
||||
.pr_8()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.cursor_pointer()
|
||||
.on_click({
|
||||
let buffer = buffer.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.handle_file_click(buffer.clone(), window, cx);
|
||||
})
|
||||
})
|
||||
.tooltip(
|
||||
Tooltip::text(format!("Review {}", path.display()))
|
||||
)
|
||||
.child(file_icon)
|
||||
.child(
|
||||
h_flex()
|
||||
.children(parent_label)
|
||||
.children(name_label),
|
||||
) // TODO: show lines changed
|
||||
.child(
|
||||
Label::new("+")
|
||||
.color(Color::Created),
|
||||
)
|
||||
.child(
|
||||
Label::new("-")
|
||||
.color(Color::Deleted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w_8()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(
|
||||
editor_bg_color,
|
||||
1.,
|
||||
),
|
||||
linear_color_stop(
|
||||
editor_bg_color
|
||||
.opacity(0.2),
|
||||
0.,
|
||||
),
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
Some(element)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||
this.profile_selector
|
||||
.read(cx)
|
||||
.menu_handle()
|
||||
.toggle(window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(editor_bg_color)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(h_flex().justify_between().child(self.context_strip.clone()))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: editor_bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
).into_any()
|
||||
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |window, cx| {
|
||||
inline_context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
|
||||
Some(inline_context_picker.clone())
|
||||
})
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
|
||||
- px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(h_flex().gap_2().child(self.profile_selector.clone()))
|
||||
.child(
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("submit-message")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
is_editor_empty
|
||||
|| !is_model_selected
|
||||
|| is_generating
|
||||
|| self.waiting_for_summaries_to_send
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Submit")
|
||||
.size(LabelSize::Small)
|
||||
.color(submit_label_color),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&Chat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| {
|
||||
binding
|
||||
.when(vim_mode_enabled, |kb| {
|
||||
kb.size(rems_from_px(12.))
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Type a message to submit",
|
||||
))
|
||||
})
|
||||
.when(is_generating, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Cancel to submit a new message",
|
||||
))
|
||||
})
|
||||
.when(!is_model_selected, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Select a model to continue",
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
.when(total_token_usage.ratio != TokenUsageRatio::Normal, |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.bg(cx.theme().status().warning_background.opacity(0.1))
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.h(line_height)
|
||||
.justify_center()
|
||||
.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.color(Color::Warning)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.mr_auto()
|
||||
.child(Label::new("Thread reaching the token limit soon").size(LabelSize::Small))
|
||||
.child(
|
||||
Label::new(
|
||||
"Start a new thread from a summary to continue the conversation.",
|
||||
)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("new-thread", "Start New Thread")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let from_thread_id = Some(this.thread.read(cx).id().clone());
|
||||
|
||||
window.dispatch_action(Box::new(NewThread {
|
||||
from_thread_id
|
||||
}), cx);
|
||||
}))
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.label_size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
189
crates/agent/src/profile_selector.rs
Normal file
189
crates/agent/src/profile_selector.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
|
||||
use indexmap::IndexMap;
|
||||
use language_model::LanguageModelRegistry;
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: IndexMap<AgentProfileId, AgentProfile>,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
focus_handle: FocusHandle,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
profiles: IndexMap::default(),
|
||||
fs,
|
||||
thread_store,
|
||||
focus_handle,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.refresh_profiles(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
|
||||
self.menu_handle.clone()
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.profiles = settings.profiles.clone();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let icon_position = IconPosition::End;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(&profile_id, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.header("Customize Current Profile");
|
||||
menu = menu.item(ContextMenuEntry::new("Tools…").handler({
|
||||
let profile_id = settings.default_profile.clone();
|
||||
move |window, cx| {
|
||||
window.dispatch_action(
|
||||
ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}));
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(ContextMenuEntry::new("Configure Profiles…").handler(
|
||||
move |window, cx| {
|
||||
window.dispatch_action(ManageProfiles::default().boxed_clone(), cx);
|
||||
},
|
||||
));
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile_id = &settings.default_profile;
|
||||
let profile = settings.profiles.get(profile_id);
|
||||
|
||||
let selected_profile = profile
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let supports_tools = model_registry
|
||||
.default_model()
|
||||
.map_or(false, |default| default.model.supports_tools());
|
||||
|
||||
let icon = match profile_id.as_str() {
|
||||
"write" => IconName::Pencil,
|
||||
"ask" => IconName::MessageBubbles,
|
||||
_ => IconName::UserRoundPen,
|
||||
};
|
||||
|
||||
let this = cx.entity().clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
PopoverMenu::new("profile-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger(if supports_tools {
|
||||
ButtonLike::new("profile-selector-button").child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
||||
.child(
|
||||
Label::new(selected_profile)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(div().opacity(0.5).children({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
ButtonLike::new("tools-not-supported-button")
|
||||
.disabled(true)
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
Label::new("No Tools")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.tooltip(Tooltip::text("The current model does not support tools."))
|
||||
})
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use client::telemetry::Telemetry;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
|
||||
use language_model::{report_assistant_event, LanguageModelRegistry, LanguageModelRequest};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, report_assistant_event,
|
||||
};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
@@ -31,7 +33,9 @@ impl TerminalCodegen {
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -3,18 +3,18 @@ use crate::context_store::ContextStore;
|
||||
use crate::inline_prompt_editor::{
|
||||
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
|
||||
};
|
||||
use crate::terminal_codegen::{CodegenEvent, TerminalCodegen, CLEAR_INPUT};
|
||||
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{actions::SelectAll, MultiBuffer};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, Focusable, Global, Subscription, UpdateGlobal, WeakEntity};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use std::sync::Arc;
|
||||
@@ -22,7 +22,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -75,7 +75,8 @@ impl TerminalInlineAssistant {
|
||||
let assist_id = self.next_assist_id.post_inc();
|
||||
let prompt_buffer =
|
||||
cx.new(|cx| MultiBuffer::singleton(cx.new(|cx| Buffer::local(String::new(), cx)), cx));
|
||||
let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
|
||||
let context_store =
|
||||
cx.new(|_cx| ContextStore::new(workspace.clone(), thread_store.clone()));
|
||||
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
|
||||
|
||||
let prompt_editor = cx.new(|cx| {
|
||||
@@ -252,7 +253,8 @@ impl TerminalInlineAssistant {
|
||||
|
||||
attach_context_to_message(
|
||||
&mut request_message,
|
||||
assist.context_store.read(cx).snapshot(cx),
|
||||
assist.context_store.read(cx).context().iter(),
|
||||
cx,
|
||||
);
|
||||
|
||||
request_message.content.push(prompt.into());
|
||||
@@ -284,7 +286,9 @@ impl TerminalInlineAssistant {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
{
|
||||
let codegen = assist.codegen.read(cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
report_assistant_event(
|
||||
File diff suppressed because it is too large
Load Diff
589
crates/agent/src/thread_history.rs
Normal file
589
crates/agent/src/thread_history.rs
Normal file
@@ -0,0 +1,589 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Entity, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity,
|
||||
Window, uniform_list,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
use crate::{AssistantPanel, RemoveSelectedThread};
|
||||
|
||||
pub struct ThreadHistory {
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
search_query: SharedString,
|
||||
search_editor: Entity<Editor>,
|
||||
all_entries: Arc<Vec<HistoryEntry>>,
|
||||
matches: Vec<StringMatch>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_search_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let search_editor_subscription =
|
||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
let query = search_editor.read(cx).text(cx);
|
||||
this.search_query = query.into();
|
||||
this.update_search(cx);
|
||||
}
|
||||
});
|
||||
|
||||
let entries: Arc<Vec<_>> = history_store
|
||||
.update(cx, |store, cx| store.entries(cx))
|
||||
.into();
|
||||
|
||||
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
||||
this.update_all_entries(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
assistant_panel,
|
||||
history_store,
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
selected_index: 0,
|
||||
search_query: SharedString::new_static(""),
|
||||
all_entries: entries,
|
||||
matches: Vec::new(),
|
||||
search_editor,
|
||||
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||
_search_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
|
||||
self.all_entries = self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.entries(cx))
|
||||
.into();
|
||||
self.matches.clear();
|
||||
self.update_search(cx);
|
||||
}
|
||||
|
||||
fn update_search(&mut self, cx: &mut Context<Self>) {
|
||||
self._search_task.take();
|
||||
|
||||
if self.has_search_query() {
|
||||
self.perform_search(cx);
|
||||
} else {
|
||||
self.matches.clear();
|
||||
self.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_search(&mut self, cx: &mut Context<Self>) {
|
||||
let query = self.search_query.clone();
|
||||
let all_entries = self.all_entries.clone();
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let executor = cx.background_executor().clone();
|
||||
|
||||
let matches = cx
|
||||
.background_spawn(async move {
|
||||
let mut candidates = Vec::with_capacity(all_entries.len());
|
||||
|
||||
for (idx, entry) in all_entries.iter().enumerate() {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
candidates.push(StringMatchCandidate::new(idx, &thread.summary));
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
candidates.push(StringMatchCandidate::new(idx, &context.title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.matches = matches;
|
||||
this.set_selected_index(0, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
|
||||
self._search_task = Some(task);
|
||||
}
|
||||
|
||||
fn has_search_query(&self) -> bool {
|
||||
!self.search_query.is_empty()
|
||||
}
|
||||
|
||||
fn matched_count(&self) -> usize {
|
||||
if self.has_search_query() {
|
||||
self.matches.len()
|
||||
} else {
|
||||
self.all_entries.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_match(&self, ix: usize) -> Option<&HistoryEntry> {
|
||||
if self.has_search_query() {
|
||||
self.matches
|
||||
.get(ix)
|
||||
.and_then(|m| self.all_entries.get(m.candidate_id))
|
||||
} else {
|
||||
self.all_entries.get(ix)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_index(count - 1, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index - 1, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(
|
||||
&mut self,
|
||||
_: &menu::SelectNext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
if self.selected_index == count - 1 {
|
||||
self.set_selected_index(0, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index + 1, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
self.set_selected_index(0, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = self.matched_count();
|
||||
if count > 0 {
|
||||
self.set_selected_index(count - 1, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
|
||||
self.selected_index = index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(index, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.assistant_panel
|
||||
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx)),
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(task) = task_result.log_err() {
|
||||
task.detach_and_log_err(cx);
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_selected_thread(
|
||||
&mut self,
|
||||
_: &RemoveSelectedThread,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(entry) = self.get_match(self.selected_index) {
|
||||
let task_result = match entry {
|
||||
HistoryEntry::Thread(thread) => self
|
||||
.assistant_panel
|
||||
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
|
||||
HistoryEntry::Context(context) => self
|
||||
.assistant_panel
|
||||
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
|
||||
};
|
||||
|
||||
if let Some(task) = task_result.log_err() {
|
||||
task.detach_and_log_err(cx);
|
||||
};
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ThreadHistory {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.search_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let selected_index = self.selected_index;
|
||||
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::remove_selected_thread))
|
||||
.when(!self.all_entries.is_empty(), |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.h(px(41.)) // Match the toolbar perfectly
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(self.search_editor.clone()),
|
||||
)
|
||||
})
|
||||
.child({
|
||||
let view = v_flex().overflow_hidden().flex_grow();
|
||||
|
||||
if self.all_entries.is_empty() {
|
||||
view.justify_center()
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("You don't have any past threads yet.")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else if self.has_search_query() && self.matches.is_empty() {
|
||||
view.justify_center().child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("No threads match your search.").size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
view.p_1().child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"thread-history",
|
||||
self.matched_count(),
|
||||
move |history, range, _window, _cx| {
|
||||
let range_start = range.start;
|
||||
let assistant_panel = history.assistant_panel.clone();
|
||||
|
||||
let render_item = |index: usize,
|
||||
entry: &HistoryEntry,
|
||||
highlight_positions: Vec<usize>|
|
||||
-> Div {
|
||||
h_flex().w_full().pb_1().child(match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
assistant_panel.clone(),
|
||||
selected_index == index + range_start,
|
||||
highlight_positions,
|
||||
)
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
assistant_panel.clone(),
|
||||
selected_index == index + range_start,
|
||||
highlight_positions,
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
};
|
||||
|
||||
if history.has_search_query() {
|
||||
history.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, m)| {
|
||||
history.all_entries.get(m.candidate_id).map(|entry| {
|
||||
render_item(index, entry, m.positions.clone())
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
history.all_entries[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| render_item(index, entry, vec![]))
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: SerializedThreadMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(
|
||||
thread: SerializedThreadMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
assistant_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.thread.summary;
|
||||
|
||||
let thread_timestamp = time_format::format_localized_timestamp(
|
||||
OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(),
|
||||
OffsetDateTime::now_utc(),
|
||||
self.assistant_panel
|
||||
.update(cx, |this, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(SharedString::from(self.thread.id.to_string()))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div().max_w_4_5().child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Thread")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text_disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Delete Thread",
|
||||
&RemoveSelectedThread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, _window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx).detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread(&id, window, cx).detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastContext {
|
||||
context: SavedContextMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
}
|
||||
|
||||
impl PastContext {
|
||||
pub fn new(
|
||||
context: SavedContextMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
highlight_positions: Vec<usize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
assistant_panel,
|
||||
selected,
|
||||
highlight_positions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastContext {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.context.title;
|
||||
let context_timestamp = time_format::format_localized_timestamp(
|
||||
OffsetDateTime::from_unix_timestamp(self.context.mtime.timestamp()).unwrap(),
|
||||
OffsetDateTime::now_utc(),
|
||||
self.assistant_panel
|
||||
.update(cx, |this, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(SharedString::from(
|
||||
self.context.path.to_string_lossy().to_string(),
|
||||
))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div().max_w_4_5().child(
|
||||
HighlightedLabel::new(summary, self.highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Prompt Editor")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text_disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(context_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Delete Prompt Editor"))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, _window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,31 @@ use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::{self, BoxFuture, Shared};
|
||||
use gpui::{
|
||||
prelude::*, App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Task,
|
||||
App, BackgroundExecutor, Context, Entity, Global, ReadGlobal, SharedString, Subscription, Task,
|
||||
prelude::*,
|
||||
};
|
||||
use heed::types::SerdeBincode;
|
||||
use heed::Database;
|
||||
use heed::types::SerdeBincode;
|
||||
use language_model::{LanguageModelToolUseId, Role, TokenUsage};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
|
||||
use crate::thread::{
|
||||
DetailedSummaryState, MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId,
|
||||
};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
ThreadsDatabase::init(cx);
|
||||
@@ -36,6 +39,7 @@ pub struct ThreadStore {
|
||||
context_server_manager: Entity<ContextServerManager>,
|
||||
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
|
||||
threads: Vec<SerializedThreadMetadata>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ThreadStore {
|
||||
@@ -50,6 +54,10 @@ impl ThreadStore {
|
||||
let context_server_manager = cx.new(|cx| {
|
||||
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
|
||||
});
|
||||
let settings_subscription =
|
||||
cx.observe_global::<SettingsStore>(move |this: &mut Self, cx| {
|
||||
this.load_default_profile(cx);
|
||||
});
|
||||
|
||||
let this = Self {
|
||||
project,
|
||||
@@ -58,6 +66,7 @@ impl ThreadStore {
|
||||
context_server_manager,
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
@@ -165,8 +174,9 @@ impl ThreadStore {
|
||||
let database = database_future.await.map_err(|err| anyhow!(err))?;
|
||||
database.delete_thread(id.clone()).await?;
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.threads.retain(|thread| thread.id != id)
|
||||
this.update(cx, |this, cx| {
|
||||
this.threads.retain(|thread| thread.id != id);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -193,15 +203,15 @@ impl ThreadStore {
|
||||
self.load_profile_by_id(&assistant_settings.default_profile, cx);
|
||||
}
|
||||
|
||||
pub fn load_profile_by_id(&self, profile_id: &Arc<str>, cx: &Context<Self>) {
|
||||
pub fn load_profile_by_id(&self, profile_id: &AgentProfileId, cx: &Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
|
||||
self.load_profile(profile);
|
||||
self.load_profile(profile, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_profile(&self, profile: &AgentProfile) {
|
||||
pub fn load_profile(&self, profile: &AgentProfile, cx: &Context<Self>) {
|
||||
self.tools.disable_all_tools();
|
||||
self.tools.enable(
|
||||
ToolSource::Native,
|
||||
@@ -212,17 +222,28 @@ impl ThreadStore {
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
if profile.enable_all_context_servers {
|
||||
for context_server in self.context_server_manager.read(cx).all_servers() {
|
||||
self.tools.enable_source(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server.id().into(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,8 +294,9 @@ impl ThreadStore {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.context_server_tool_ids.insert(server_id, tool_ids);
|
||||
this.load_default_profile(cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
@@ -287,6 +309,7 @@ impl ThreadStore {
|
||||
context_server::manager::Event::ServerStopped { server_id } => {
|
||||
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
|
||||
tool_working_set.remove(&tool_ids);
|
||||
self.load_default_profile(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +323,7 @@ pub struct SerializedThreadMetadata {
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedThread {
|
||||
pub version: String,
|
||||
pub summary: SharedString,
|
||||
@@ -310,6 +333,8 @@ pub struct SerializedThread {
|
||||
pub initial_project_snapshot: Option<Arc<ProjectSnapshot>>,
|
||||
#[serde(default)]
|
||||
pub cumulative_token_usage: TokenUsage,
|
||||
#[serde(default)]
|
||||
pub detailed_summary_state: DetailedSummaryState,
|
||||
}
|
||||
|
||||
impl SerializedThread {
|
||||
@@ -350,6 +375,8 @@ pub struct SerializedMessage {
|
||||
pub tool_uses: Vec<SerializedToolUse>,
|
||||
#[serde(default)]
|
||||
pub tool_results: Vec<SerializedToolResult>,
|
||||
#[serde(default)]
|
||||
pub context: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -393,6 +420,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(),
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -416,6 +444,7 @@ impl LegacySerializedMessage {
|
||||
segments: vec![SerializedMessageSegment::Text { text: self.text }],
|
||||
tool_uses: self.tool_uses,
|
||||
tool_results: self.tool_results,
|
||||
context: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{Tool, ToolWorkingSet};
|
||||
use collections::HashMap;
|
||||
use futures::future::Shared;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Shared;
|
||||
use gpui::{App, SharedString, Task};
|
||||
use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
|
||||
@@ -23,6 +23,7 @@ pub struct ToolUse {
|
||||
pub status: ToolUseStatus,
|
||||
pub input: serde_json::Value,
|
||||
pub icon: ui::IconName,
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -34,6 +35,18 @@ pub enum ToolUseStatus {
|
||||
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 struct ToolUseState {
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
@@ -42,6 +55,8 @@ pub struct ToolUseState {
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
}
|
||||
|
||||
pub const USING_TOOL_MARKER: &str = "<using_tool>";
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>) -> Self {
|
||||
Self {
|
||||
@@ -112,6 +127,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id,
|
||||
tool_name: tool_use.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
},
|
||||
@@ -133,6 +149,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id,
|
||||
tool_name: tool_use.name.clone(),
|
||||
content: "Tool canceled by user".into(),
|
||||
is_error: true,
|
||||
},
|
||||
@@ -181,10 +198,11 @@ impl ToolUseState {
|
||||
}
|
||||
})();
|
||||
|
||||
let icon = if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
|
||||
tool.icon()
|
||||
let (icon, needs_confirmation) = if let Some(tool) = self.tools.tool(&tool_use.name, cx)
|
||||
{
|
||||
(tool.icon(), tool.needs_confirmation())
|
||||
} else {
|
||||
IconName::Cog
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
|
||||
tool_uses.push(ToolUse {
|
||||
@@ -194,6 +212,7 @@ impl ToolUseState {
|
||||
input: tool_use.input.clone(),
|
||||
status,
|
||||
icon,
|
||||
needs_confirmation,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -209,7 +228,7 @@ impl ToolUseState {
|
||||
if let Some(tool) = self.tools.tool(tool_name, cx) {
|
||||
tool.ui_text(input).into()
|
||||
} else {
|
||||
"Unknown tool".into()
|
||||
format!("Unknown tool {tool_name:?}").into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +329,7 @@ impl ToolUseState {
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
output: Result<String>,
|
||||
) -> Option<PendingToolUse> {
|
||||
match output {
|
||||
@@ -318,6 +338,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: tool_result.into(),
|
||||
is_error: false,
|
||||
},
|
||||
@@ -329,6 +350,7 @@ impl ToolUseState {
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: err.to_string().into(),
|
||||
is_error: true,
|
||||
},
|
||||
@@ -349,8 +371,28 @@ impl ToolUseState {
|
||||
request_message: &mut LanguageModelRequestMessage,
|
||||
) {
|
||||
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
|
||||
let mut found_tool_use = false;
|
||||
|
||||
for tool_use in tool_uses {
|
||||
if self.tool_results.contains_key(&tool_use.id) {
|
||||
if !found_tool_use {
|
||||
// The API fails if a message contains a tool use without any (non-whitespace) text around it
|
||||
match request_message.content.last_mut() {
|
||||
Some(MessageContent::Text(txt)) => {
|
||||
if txt.is_empty() {
|
||||
txt.push_str(USING_TOOL_MARKER);
|
||||
}
|
||||
}
|
||||
None | Some(_) => {
|
||||
request_message
|
||||
.content
|
||||
.push(MessageContent::Text(USING_TOOL_MARKER.into()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
found_tool_use = true;
|
||||
|
||||
// Do not send tool uses until they are completed
|
||||
request_message
|
||||
.content
|
||||
@@ -376,6 +418,7 @@ impl ToolUseState {
|
||||
request_message.content.push(MessageContent::ToolResult(
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name: tool_result.tool_name.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: if tool_result.content.is_empty() {
|
||||
// Surprisingly, the API fails if we return an empty string here.
|
||||
5
crates/agent/src/ui.rs
Normal file
5
crates/agent/src/ui.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod agent_notification;
|
||||
mod context_pill;
|
||||
|
||||
pub use agent_notification::*;
|
||||
pub use context_pill::*;
|
||||
@@ -1,24 +1,29 @@
|
||||
use gpui::{
|
||||
point, App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
|
||||
App, Context, EventEmitter, IntoElement, PlatformDisplay, Size, Window,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||
linear_color_stop, linear_gradient, point,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use std::rc::Rc;
|
||||
use theme;
|
||||
use ui::{prelude::*, Render};
|
||||
use ui::{Render, prelude::*};
|
||||
|
||||
pub struct ToolReadyPopUp {
|
||||
pub struct AgentNotification {
|
||||
title: SharedString,
|
||||
caption: SharedString,
|
||||
icon: IconName,
|
||||
icon_color: Color,
|
||||
}
|
||||
|
||||
impl ToolReadyPopUp {
|
||||
pub fn new(caption: impl Into<SharedString>, icon: IconName, icon_color: Color) -> Self {
|
||||
impl AgentNotification {
|
||||
pub fn new(
|
||||
title: impl Into<SharedString>,
|
||||
caption: impl Into<SharedString>,
|
||||
icon: IconName,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
caption: caption.into(),
|
||||
icon,
|
||||
icon_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +63,35 @@ impl ToolReadyPopUp {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ToolReadyPopupEvent {
|
||||
pub enum AgentNotificationEvent {
|
||||
Accepted,
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolReadyPopupEvent> for ToolReadyPopUp {}
|
||||
impl EventEmitter<AgentNotificationEvent> for AgentNotification {}
|
||||
|
||||
impl Render for ToolReadyPopUp {
|
||||
impl Render for AgentNotification {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let line_height = window.line_height();
|
||||
|
||||
let bg = cx.theme().colors().elevated_surface_background;
|
||||
let gradient_overflow = || {
|
||||
div()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w_8()
|
||||
.bottom_0()
|
||||
.right_0()
|
||||
.bg(linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(bg, 1.),
|
||||
linear_color_stop(bg.opacity(0.2), 0.),
|
||||
))
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("agent-notification")
|
||||
.size_full()
|
||||
.p_3()
|
||||
.gap_4()
|
||||
@@ -80,48 +101,62 @@ impl Render for ToolReadyPopUp {
|
||||
.font(ui_font)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_xl()
|
||||
.on_click(cx.listener(|_, _, _, cx| {
|
||||
cx.emit(AgentNotificationEvent::Accepted);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.flex_1()
|
||||
.child(
|
||||
h_flex().h(line_height).justify_center().child(
|
||||
Icon::new(self.icon)
|
||||
.color(self.icon_color)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.max_w(px(300.))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(16.))
|
||||
.relative()
|
||||
.text_size(px(14.))
|
||||
.text_color(cx.theme().colors().text)
|
||||
.child("Agent Panel"),
|
||||
.truncate()
|
||||
.child(self.title.clone())
|
||||
.child(gradient_overflow()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(14.))
|
||||
.relative()
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(self.caption.clone()),
|
||||
.truncate()
|
||||
.child(self.caption.clone())
|
||||
.child(gradient_overflow()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new("open", "View Panel")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click({
|
||||
cx.listener(move |_this, _event, _, cx| {
|
||||
cx.emit(ToolReadyPopupEvent::Accepted);
|
||||
cx.emit(AgentNotificationEvent::Accepted);
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click({
|
||||
.child(Button::new("dismiss", "Dismiss").full_width().on_click({
|
||||
cx.listener(move |_, _event, _, cx| {
|
||||
cx.emit(ToolReadyPopupEvent::Dismissed);
|
||||
cx.emit(AgentNotificationEvent::Dismissed);
|
||||
})
|
||||
})),
|
||||
)
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use gpui::ClickEvent;
|
||||
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||
use gpui::{Animation, AnimationExt as _, pulsating_between};
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*};
|
||||
|
||||
use crate::context::{ContextKind, ContextSnapshot};
|
||||
use crate::context::{AssistantContext, ContextId, ContextKind};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub enum ContextPill {
|
||||
Added {
|
||||
context: ContextSnapshot,
|
||||
context: AddedContext,
|
||||
dupe_name: bool,
|
||||
focused: bool,
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
@@ -25,7 +27,7 @@ pub enum ContextPill {
|
||||
|
||||
impl ContextPill {
|
||||
pub fn added(
|
||||
context: ContextSnapshot,
|
||||
context: AddedContext,
|
||||
dupe_name: bool,
|
||||
focused: bool,
|
||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
@@ -77,17 +79,21 @@ impl ContextPill {
|
||||
|
||||
pub fn icon(&self) -> Icon {
|
||||
match self {
|
||||
Self::Added { context, .. } => match &context.icon_path {
|
||||
Some(icon_path) => Icon::from_path(icon_path),
|
||||
None => Icon::new(context.kind.icon()),
|
||||
},
|
||||
Self::Suggested {
|
||||
icon_path: Some(icon_path),
|
||||
..
|
||||
}
|
||||
| Self::Added {
|
||||
context:
|
||||
AddedContext {
|
||||
icon_path: Some(icon_path),
|
||||
..
|
||||
},
|
||||
..
|
||||
} => Icon::from_path(icon_path),
|
||||
Self::Suggested {
|
||||
kind,
|
||||
icon_path: None,
|
||||
Self::Suggested { kind, .. }
|
||||
| Self::Added {
|
||||
context: AddedContext { kind, .. },
|
||||
..
|
||||
} => Icon::new(kind.icon()),
|
||||
}
|
||||
@@ -144,7 +150,7 @@ impl RenderOnce for ContextPill {
|
||||
element
|
||||
}
|
||||
})
|
||||
.when_some(context.tooltip.clone(), |element, tooltip| {
|
||||
.when_some(context.tooltip.as_ref(), |element, tooltip| {
|
||||
element.tooltip(Tooltip::text(tooltip.clone()))
|
||||
}),
|
||||
)
|
||||
@@ -162,7 +168,25 @@ impl RenderOnce for ContextPill {
|
||||
})
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
element.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
element
|
||||
.cursor_pointer()
|
||||
.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
})
|
||||
.map(|element| {
|
||||
if context.summarizing {
|
||||
element
|
||||
.tooltip(ui::Tooltip::text("Summarizing..."))
|
||||
.with_animation(
|
||||
"pulsating-ctx-pill",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.4, 0.8)),
|
||||
|label, delta| label.opacity(delta),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
element.into_any()
|
||||
}
|
||||
}),
|
||||
ContextPill::Suggested {
|
||||
name,
|
||||
@@ -173,10 +197,14 @@ impl RenderOnce for ContextPill {
|
||||
} => 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
|
||||
} else {
|
||||
color.border_variant.opacity(0.5)
|
||||
color.border
|
||||
})
|
||||
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||
.child(
|
||||
@@ -190,9 +218,10 @@ impl RenderOnce for ContextPill {
|
||||
.child(
|
||||
Label::new(match kind {
|
||||
ContextKind::File => "Active Tab",
|
||||
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
|
||||
"Active"
|
||||
}
|
||||
ContextKind::Thread
|
||||
| ContextKind::Directory
|
||||
| ContextKind::FetchedUrl
|
||||
| ContextKind::Symbol => "Active",
|
||||
})
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
@@ -208,7 +237,105 @@ impl RenderOnce for ContextPill {
|
||||
.when_some(on_click.as_ref(), |element, on_click| {
|
||||
let on_click = on_click.clone();
|
||||
element.on_click(move |event, window, cx| on_click(event, window, cx))
|
||||
}),
|
||||
})
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddedContext {
|
||||
pub id: ContextId,
|
||||
pub kind: ContextKind,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub summarizing: bool,
|
||||
}
|
||||
|
||||
impl AddedContext {
|
||||
pub fn new(context: &AssistantContext, cx: &App) -> AddedContext {
|
||||
match context {
|
||||
AssistantContext::File(file_context) => {
|
||||
let full_path = file_context.context_buffer.file.full_path(cx);
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: file_context.id,
|
||||
kind: ContextKind::File,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: FileIcons::get_icon(&full_path, cx),
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Directory(directory_context) => {
|
||||
// TODO: handle worktree disambiguation. Maybe by storing an `Arc<dyn File>` to also
|
||||
// handle renames?
|
||||
let full_path = &directory_context.project_path.path;
|
||||
let full_path_string: SharedString =
|
||||
full_path.to_string_lossy().into_owned().into();
|
||||
let name = full_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned().into())
|
||||
.unwrap_or_else(|| full_path_string.clone());
|
||||
let parent = full_path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|n| n.to_string_lossy().into_owned().into());
|
||||
AddedContext {
|
||||
id: directory_context.id,
|
||||
kind: ContextKind::Directory,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path_string),
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
}
|
||||
}
|
||||
|
||||
AssistantContext::Symbol(symbol_context) => AddedContext {
|
||||
id: symbol_context.id,
|
||||
kind: ContextKind::Symbol,
|
||||
name: symbol_context.context_symbol.id.name.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::FetchedUrl(fetched_url_context) => AddedContext {
|
||||
id: fetched_url_context.id,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
name: fetched_url_context.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: false,
|
||||
},
|
||||
|
||||
AssistantContext::Thread(thread_context) => AddedContext {
|
||||
id: thread_context.id,
|
||||
kind: ContextKind::Thread,
|
||||
name: thread_context.summary(cx),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
summarizing: thread_context
|
||||
.thread
|
||||
.read(cx)
|
||||
.is_generating_detailed_summary(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,3 +26,4 @@ serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
mod supported_countries;
|
||||
|
||||
use std::{pin::Pin, str::FromStr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use http_client::http::{HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -220,16 +220,23 @@ impl Model {
|
||||
.map(|header| header.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Self::Custom {
|
||||
extra_beta_headers, ..
|
||||
} = self
|
||||
{
|
||||
headers.extend(
|
||||
extra_beta_headers
|
||||
.iter()
|
||||
.filter(|header| !header.trim().is_empty())
|
||||
.cloned(),
|
||||
);
|
||||
match self {
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
|
||||
// Try beta token-efficient tool use (supported in Claude 3.7 Sonnet only)
|
||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use/token-efficient-tool-use
|
||||
headers.push("token-efficient-tools-2025-02-19".to_string());
|
||||
}
|
||||
Self::Custom {
|
||||
extra_beta_headers, ..
|
||||
} => {
|
||||
headers.extend(
|
||||
extra_beta_headers
|
||||
.iter()
|
||||
.filter(|header| !header.trim().is_empty())
|
||||
.cloned(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
headers.join(",")
|
||||
@@ -437,50 +444,6 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn extract_tool_args_from_events(
|
||||
tool_name: String,
|
||||
mut events: Pin<Box<dyn Send + Stream<Item = Result<Event>>>>,
|
||||
) -> Result<impl Send + Stream<Item = Result<String>>> {
|
||||
let mut tool_use_index = None;
|
||||
while let Some(event) = events.next().await {
|
||||
if let Event::ContentBlockStart {
|
||||
index,
|
||||
content_block: ResponseContent::ToolUse { name, .. },
|
||||
} = event?
|
||||
{
|
||||
if name == tool_name {
|
||||
tool_use_index = Some(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(tool_use_index) = tool_use_index else {
|
||||
return Err(anyhow!("tool not used"));
|
||||
};
|
||||
|
||||
Ok(events.filter_map(move |event| {
|
||||
let result = match event {
|
||||
Err(error) => Some(Err(error)),
|
||||
Ok(Event::ContentBlockDelta { index, delta }) => match delta {
|
||||
ContentDelta::TextDelta { .. } => None,
|
||||
ContentDelta::ThinkingDelta { .. } => None,
|
||||
ContentDelta::SignatureDelta { .. } => None,
|
||||
ContentDelta::InputJsonDelta { partial_json } => {
|
||||
if index == tool_use_index {
|
||||
Some(Ok(partial_json))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
async move { result }
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CacheControlType {
|
||||
|
||||
@@ -19,3 +19,4 @@ smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -5,9 +5,9 @@ use std::time::Duration;
|
||||
use anyhow::Context as _;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
#[cfg(unix)]
|
||||
use futures::{io::BufReader, AsyncBufReadExt as _};
|
||||
use futures::{AsyncBufReadExt as _, io::BufReader};
|
||||
#[cfg(unix)]
|
||||
use futures::{select_biased, AsyncWriteExt as _, FutureExt as _};
|
||||
use futures::{AsyncWriteExt as _, FutureExt as _, select_biased};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use gpui::{AsyncApp, BackgroundExecutor, Task};
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -15,3 +15,4 @@ workspace = true
|
||||
anyhow.workspace = true
|
||||
gpui.workspace = true
|
||||
rust-embed.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -57,10 +57,11 @@ impl Assets {
|
||||
|
||||
pub fn load_test_fonts(&self, cx: &App) {
|
||||
cx.text_system()
|
||||
.add_fonts(vec![self
|
||||
.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
|
||||
.unwrap()
|
||||
.unwrap()])
|
||||
.add_fonts(vec![
|
||||
self.load("fonts/plex-mono/ZedPlexMono-Regular.ttf")
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ lsp.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
prompt_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
@@ -56,7 +55,6 @@ proto.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
@@ -71,6 +69,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -10,17 +10,15 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use assistant_slash_commands::{ProjectSlashCommandFeatureFlag, SearchSlashCommandFeatureFlag};
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, App, Global, ReadGlobal, UpdateGlobal};
|
||||
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions};
|
||||
use language_model::{
|
||||
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
|
||||
};
|
||||
use prompt_store::PromptBuilder;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticDb};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
@@ -102,33 +100,6 @@ pub fn init(
|
||||
AssistantSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
async move |cx| {
|
||||
let is_search_slash_command_enabled = cx
|
||||
.update(|cx| cx.wait_for_flag::<SearchSlashCommandFeatureFlag>())?
|
||||
.await;
|
||||
let is_project_slash_command_enabled = cx
|
||||
.update(|cx| cx.wait_for_flag::<ProjectSlashCommandFeatureFlag>())?
|
||||
.await;
|
||||
|
||||
if !is_search_slash_command_enabled && !is_project_slash_command_enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let embedding_provider = CloudEmbeddingProvider::new(client.clone());
|
||||
let semantic_index = SemanticDb::new(
|
||||
paths::embeddings_dir().join("semantic-index-db.0.mdb"),
|
||||
Arc::new(embedding_provider),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
cx.update(|cx| cx.set_global(semantic_index))
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
assistant_context_editor::init(client.clone(), cx);
|
||||
prompt_library::init(cx);
|
||||
init_language_model_settings(cx);
|
||||
@@ -137,7 +108,7 @@ pub fn init(
|
||||
assistant_panel::init(cx);
|
||||
context_server::init(cx);
|
||||
|
||||
register_slash_commands(Some(prompt_builder.clone()), cx);
|
||||
register_slash_commands(cx);
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
prompt_builder.clone(),
|
||||
@@ -190,12 +161,38 @@ fn init_language_model_settings(cx: &mut App) {
|
||||
|
||||
fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
// Default model - used as fallback
|
||||
let active_model_provider_name =
|
||||
LanguageModelProviderId::from(settings.default_model.provider.clone());
|
||||
let active_model_id = LanguageModelId::from(settings.default_model.model.clone());
|
||||
let editor_provider_name =
|
||||
LanguageModelProviderId::from(settings.editor_model.provider.clone());
|
||||
let editor_model_id = LanguageModelId::from(settings.editor_model.model.clone());
|
||||
|
||||
// Inline assistant model
|
||||
let inline_assistant_model = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let inline_assistant_provider_name =
|
||||
LanguageModelProviderId::from(inline_assistant_model.provider.clone());
|
||||
let inline_assistant_model_id = LanguageModelId::from(inline_assistant_model.model.clone());
|
||||
|
||||
// Commit message model
|
||||
let commit_message_model = settings
|
||||
.commit_message_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let commit_message_provider_name =
|
||||
LanguageModelProviderId::from(commit_message_model.provider.clone());
|
||||
let commit_message_model_id = LanguageModelId::from(commit_message_model.model.clone());
|
||||
|
||||
// Thread summary model
|
||||
let thread_summary_model = settings
|
||||
.thread_summary_model
|
||||
.as_ref()
|
||||
.unwrap_or(&settings.default_model);
|
||||
let thread_summary_provider_name =
|
||||
LanguageModelProviderId::from(thread_summary_model.provider.clone());
|
||||
let thread_summary_model_id = LanguageModelId::from(thread_summary_model.model.clone());
|
||||
|
||||
let inline_alternatives = settings
|
||||
.inline_alternatives
|
||||
.iter()
|
||||
@@ -206,14 +203,34 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_active_model(&active_model_provider_name, &active_model_id, cx);
|
||||
registry.select_editor_model(&editor_provider_name, &editor_model_id, cx);
|
||||
// Set the default model
|
||||
registry.select_default_model(&active_model_provider_name, &active_model_id, cx);
|
||||
|
||||
// Set the specific models
|
||||
registry.select_inline_assistant_model(
|
||||
&inline_assistant_provider_name,
|
||||
&inline_assistant_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_commit_message_model(
|
||||
&commit_message_provider_name,
|
||||
&commit_message_model_id,
|
||||
cx,
|
||||
);
|
||||
registry.select_thread_summary_model(
|
||||
&thread_summary_provider_name,
|
||||
&thread_summary_model_id,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Set the alternatives
|
||||
registry.select_inline_alternative_models(inline_alternatives, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut App) {
|
||||
fn register_slash_commands(cx: &mut App) {
|
||||
let slash_command_registry = SlashCommandRegistry::global(cx);
|
||||
|
||||
slash_command_registry.register_command(assistant_slash_commands::FileSlashCommand, true);
|
||||
@@ -231,21 +248,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::FetchSlashCommand, true);
|
||||
|
||||
if let Some(prompt_builder) = prompt_builder {
|
||||
cx.observe_flag::<assistant_slash_commands::ProjectSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
if is_enabled {
|
||||
slash_command_registry.register_command(
|
||||
assistant_slash_commands::ProjectSlashCommand::new(prompt_builder.clone()),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
cx.observe_flag::<assistant_slash_commands::StreamingExampleSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
@@ -262,17 +264,6 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
|
||||
update_slash_commands_from_settings(cx);
|
||||
cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
|
||||
.detach();
|
||||
|
||||
cx.observe_flag::<assistant_slash_commands::SearchSlashCommandFeatureFlag, _>({
|
||||
let slash_command_registry = slash_command_registry.clone();
|
||||
move |is_enabled, _cx| {
|
||||
if is_enabled {
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::SearchSlashCommand, true);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_slash_commands_from_settings(cx: &mut App) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{canvas, AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription};
|
||||
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use ui::{prelude::*, ElevationIndex};
|
||||
use ui::{ElevationIndex, prelude::*};
|
||||
use workspace::Item;
|
||||
|
||||
pub struct ConfigurationView {
|
||||
|
||||
@@ -1,45 +1,47 @@
|
||||
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
|
||||
use crate::Assistant;
|
||||
use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
|
||||
use crate::{
|
||||
terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewChat,
|
||||
DeployHistory, InlineAssistant, NewChat, terminal_inline_assistant::TerminalInlineAssistant,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_context_editor::{
|
||||
make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
|
||||
ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
|
||||
ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
|
||||
DEFAULT_TAB_TITLE,
|
||||
AssistantContext, AssistantPanelDelegate, ContextEditor, ContextEditorToolbarItem,
|
||||
ContextEditorToolbarItemEvent, ContextHistory, ContextId, ContextStore, ContextStoreEvent,
|
||||
DEFAULT_TAB_TITLE, InsertDraggedFiles, SlashCommandCompletionProvider,
|
||||
make_lsp_adapter_delegate,
|
||||
};
|
||||
use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use client::{proto, Client, Status};
|
||||
use client::{Client, Status, proto};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
|
||||
Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, Task,
|
||||
UpdateGlobal, WeakEntity, prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_library::{open_prompt_library, PromptLibrary};
|
||||
use prompt_library::{PromptLibrary, open_prompt_library};
|
||||
use prompt_store::PromptBuilder;
|
||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||
use settings::{update_settings_file, Settings};
|
||||
use search::{BufferSearchBar, buffer_search::DivRegistrar};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
|
||||
use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
use util::{maybe, ResultExt};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::{ContextMenu, PopoverMenu, Tooltip, prelude::*};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::DraggedTab;
|
||||
use workspace::{
|
||||
DraggedSelection, Pane, ToggleZoom, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
|
||||
pane,
|
||||
};
|
||||
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ToggleFocus};
|
||||
use zed_actions::assistant::{InlineAssist, OpenPromptLibrary, ShowConfiguration, ToggleFocus};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
|
||||
@@ -52,7 +54,15 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(ContextEditor::insert_dragged_files)
|
||||
.register_action(AssistantPanel::show_configuration)
|
||||
.register_action(AssistantPanel::create_new_context)
|
||||
.register_action(AssistantPanel::restart_context_servers);
|
||||
.register_action(AssistantPanel::restart_context_servers)
|
||||
.register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
|
||||
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)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
@@ -297,8 +307,10 @@ impl AssistantPanel {
|
||||
&LanguageModelRegistry::global(cx),
|
||||
window,
|
||||
|this, _, event: &language_model::Event, window, cx| match event {
|
||||
language_model::Event::ActiveModelChanged
|
||||
| language_model::Event::EditorModelChanged => {
|
||||
language_model::Event::DefaultModelChanged
|
||||
| language_model::Event::InlineAssistantModelChanged
|
||||
| language_model::Event::CommitMessageModelChanged
|
||||
| language_model::Event::ThreadSummaryModelChanged => {
|
||||
this.completion_provider_changed(window, cx);
|
||||
}
|
||||
language_model::Event::ProviderStateChanged => {
|
||||
@@ -467,12 +479,12 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn update_zed_ai_notice_visibility(&mut self, client_status: Status, cx: &mut Context<Self>) {
|
||||
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
|
||||
// If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
|
||||
// the provider, we want to show a nudge to sign in.
|
||||
let show_zed_ai_notice = client_status.is_signed_out()
|
||||
&& active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
&& model.map_or(true, |model| model.provider.id().0 == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
self.show_zed_ai_notice = show_zed_ai_notice;
|
||||
cx.notify();
|
||||
@@ -540,8 +552,8 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map(|p| p.id())
|
||||
.default_model()
|
||||
.map(|default| default.provider.id())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -567,7 +579,9 @@ impl AssistantPanel {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
|
||||
let Some(ConfiguredModel { provider, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).default_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -975,8 +989,8 @@ impl AssistantPanel {
|
||||
|this, _, event: &ConfigurationViewEvent, window, cx| match event {
|
||||
ConfigurationViewEvent::NewProviderContextEditor(provider) => {
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map_or(true, |p| p.id() != provider.id())
|
||||
.default_model()
|
||||
.map_or(true, |default| default.provider.id() != provider.id())
|
||||
{
|
||||
if let Some(model) = provider.default_model(cx) {
|
||||
update_settings_file::<AssistantSettings>(
|
||||
@@ -1154,8 +1168,8 @@ impl AssistantPanel {
|
||||
|
||||
fn is_authenticated(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
.default_model()
|
||||
.map_or(false, |default| default.provider.is_authenticated(cx))
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
@@ -1163,8 +1177,8 @@ impl AssistantPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<(), AuthenticateError>>> {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_provider()
|
||||
.map_or(None, |provider| Some(provider.authenticate(cx)))
|
||||
.default_model()
|
||||
.map_or(None, |default| Some(default.provider.authenticate(cx)))
|
||||
}
|
||||
|
||||
fn restart_context_servers(
|
||||
|
||||
@@ -2,39 +2,40 @@ use crate::{
|
||||
Assistant, AssistantPanel, AssistantPanelEvent, CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_context_editor::{humanize_token_count, RequestType};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_context_editor::{RequestType, humanize_token_count};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::{telemetry::Telemetry, ErrorExt};
|
||||
use collections::{hash_map, HashMap, HashSet, VecDeque};
|
||||
use client::{ErrorExt, telemetry::Telemetry};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::{
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset as _, ToPoint,
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
display_map::{
|
||||
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
|
||||
ToDisplayPoint,
|
||||
},
|
||||
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode,
|
||||
EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot,
|
||||
ToOffset as _, ToPoint,
|
||||
};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedPro,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt,
|
||||
channel::mpsc,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
join, SinkExt, Stream, StreamExt,
|
||||
join,
|
||||
};
|
||||
use gpui::{
|
||||
anchored, deferred, point, AnyElement, App, ClickEvent, Context, CursorStyle, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, FontWeight, Global, HighlightStyle, Subscription, Task,
|
||||
TextStyle, UpdateGlobal, WeakEntity, Window,
|
||||
AnyElement, App, ClickEvent, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, FontWeight, Global, HighlightStyle, Subscription, Task, TextStyle, UpdateGlobal,
|
||||
WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language::{line_diff, Buffer, IndentKind, Point, Selection, TransactionId};
|
||||
use language::{Buffer, IndentKind, Point, Selection, TransactionId, line_diff};
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role,
|
||||
ConfiguredModel, LanguageModel, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelTextStream, Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
@@ -42,7 +43,7 @@ use parking_lot::Mutex;
|
||||
use project::{CodeAction, LspAction, ProjectTransaction};
|
||||
use prompt_store::PromptBuilder;
|
||||
use rope::Rope;
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use smol::future::FutureExt;
|
||||
use std::{
|
||||
cmp,
|
||||
@@ -61,10 +62,10 @@ use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, text_for_action, CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip,
|
||||
CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, Tooltip, prelude::*, text_for_action,
|
||||
};
|
||||
use util::{RangeExt, ResultExt};
|
||||
use workspace::{notifications::NotificationId, ItemHandle, Toast, Workspace};
|
||||
use workspace::{ItemHandle, Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -311,7 +312,9 @@ impl InlineAssistant {
|
||||
start..end,
|
||||
));
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).default_model()
|
||||
{
|
||||
self.telemetry.report_assistant_event(AssistantEvent {
|
||||
conversation_id: None,
|
||||
kind: AssistantKind::Inline,
|
||||
@@ -524,14 +527,14 @@ impl InlineAssistant {
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Above(range.start),
|
||||
height: prompt_editor_height,
|
||||
height: Some(prompt_editor_height),
|
||||
render: build_assist_editor_renderer(prompt_editor),
|
||||
priority: 0,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Below(range.end),
|
||||
height: 0,
|
||||
height: None,
|
||||
render: Arc::new(|cx| {
|
||||
v_flex()
|
||||
.h_full()
|
||||
@@ -876,7 +879,9 @@ impl InlineAssistant {
|
||||
let active_alternative = assist.codegen.read(cx).active_alternative().clone();
|
||||
let message_id = active_alternative.read(cx).message_id.clone();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).default_model()
|
||||
{
|
||||
let language_name = assist.editor.upgrade().and_then(|editor| {
|
||||
let multibuffer = editor.read(cx).buffer().read(cx);
|
||||
let multibuffer_snapshot = multibuffer.snapshot(cx);
|
||||
@@ -1269,10 +1274,7 @@ impl InlineAssistant {
|
||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer.push_excerpts(
|
||||
old_buffer.clone(),
|
||||
Some(ExcerptRange {
|
||||
context: buffer_start..buffer_end,
|
||||
primary: None,
|
||||
}),
|
||||
Some(ExcerptRange::new(buffer_start..buffer_end)),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -1299,7 +1301,7 @@ impl InlineAssistant {
|
||||
deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1);
|
||||
new_blocks.push(BlockProperties {
|
||||
placement: BlockPlacement::Above(new_row),
|
||||
height,
|
||||
height: Some(height),
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |cx| {
|
||||
div()
|
||||
@@ -1631,8 +1633,8 @@ impl Render for PromptEditor {
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.default_model()
|
||||
.map(|default| default.model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
@@ -2079,7 +2081,7 @@ impl PromptEditor {
|
||||
let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let default_model = model_registry.active_model();
|
||||
let default_model = model_registry.default_model().map(|default| default.model);
|
||||
let alternative_models = model_registry.inline_alternative_models();
|
||||
|
||||
let get_model_name = |index: usize| -> String {
|
||||
@@ -2185,7 +2187,9 @@ impl PromptEditor {
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model()?;
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()?
|
||||
.model;
|
||||
let token_counts = self.token_counts?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
@@ -2640,8 +2644,9 @@ impl Codegen {
|
||||
}
|
||||
|
||||
let primary_model = LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.context("no active model")?;
|
||||
.default_model()
|
||||
.context("no active model")?
|
||||
.model;
|
||||
|
||||
for (model, alternative) in iter::once(primary_model)
|
||||
.chain(alternative_models)
|
||||
@@ -2865,7 +2870,9 @@ impl CodegenAlternative {
|
||||
assistant_panel_context: Option<LanguageModelRequest>,
|
||||
cx: &App,
|
||||
) -> BoxFuture<'static, Result<TokenCounts>> {
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
{
|
||||
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
|
||||
match request {
|
||||
Ok(request) => {
|
||||
@@ -3710,8 +3717,8 @@ mod tests {
|
||||
use gpui::TestAppContext;
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
|
||||
tree_sitter_rust,
|
||||
};
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
use crate::{AssistantPanel, AssistantPanelEvent, DEFAULT_CONTEXT_LINES};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_context_editor::{humanize_token_count, RequestType};
|
||||
use assistant_context_editor::{RequestType, humanize_token_count};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
|
||||
actions::{MoveDown, MoveUp, SelectAll},
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, SinkExt, StreamExt};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{
|
||||
App, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, Subscription, Task,
|
||||
TextStyle, UpdateGlobal, WeakEntity,
|
||||
};
|
||||
use language::Buffer;
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, Role,
|
||||
ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
Role, report_assistant_event,
|
||||
};
|
||||
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use prompt_store::PromptBuilder;
|
||||
use settings::{update_settings_file, Settings};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use std::{
|
||||
cmp,
|
||||
sync::Arc,
|
||||
@@ -31,9 +31,9 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
|
||||
use ui::{IconButtonShape, Tooltip, prelude::*, text_for_action};
|
||||
use util::ResultExt;
|
||||
use workspace::{notifications::NotificationId, Toast, Workspace};
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -318,7 +318,9 @@ impl TerminalInlineAssistant {
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
|
||||
if let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
{
|
||||
let codegen = assist.codegen.read(cx);
|
||||
let executor = cx.background_executor().clone();
|
||||
report_assistant_event(
|
||||
@@ -652,8 +654,8 @@ impl Render for PromptEditor {
|
||||
format!(
|
||||
"Using {}",
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.map(|model| model.name().0)
|
||||
.inline_assistant_model()
|
||||
.map(|inline_assistant| inline_assistant.model.name().0)
|
||||
.unwrap_or_else(|| "No model selected".into()),
|
||||
),
|
||||
None,
|
||||
@@ -822,7 +824,9 @@ impl PromptEditor {
|
||||
|
||||
fn count_tokens(&mut self, cx: &mut Context<Self>) {
|
||||
let assist_id = self.id;
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.pending_token_count = cx.spawn(async move |this, cx| {
|
||||
@@ -980,7 +984,9 @@ impl PromptEditor {
|
||||
}
|
||||
|
||||
fn render_token_count(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model()?;
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.inline_assistant_model()?
|
||||
.model;
|
||||
let token_count = self.token_count?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
@@ -1131,7 +1137,9 @@ impl Codegen {
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut Context<Self>) {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
|
||||
Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct ProfilePicker {
|
||||
picker: Entity<Picker<ProfilePickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ProfilePicker {
|
||||
pub fn new(
|
||||
delegate: ProfilePickerDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ProfilePicker {}
|
||||
|
||||
impl Focusable for ProfilePicker {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfilePicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProfileEntry {
|
||||
pub id: Arc<str>,
|
||||
pub name: SharedString,
|
||||
}
|
||||
|
||||
pub struct ProfilePickerDelegate {
|
||||
profile_picker: WeakEntity<ProfilePicker>,
|
||||
profiles: Vec<ProfileEntry>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl ProfilePickerDelegate {
|
||||
pub fn new(
|
||||
on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
|
||||
cx: &mut Context<ProfilePicker>,
|
||||
) -> Self {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let profiles = settings
|
||||
.profiles
|
||||
.iter()
|
||||
.map(|(id, profile)| ProfileEntry {
|
||||
id: id.clone(),
|
||||
name: profile.name.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
profile_picker: cx.entity().downgrade(),
|
||||
profiles,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
on_confirm: Arc::new(on_confirm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ProfilePickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search profiles…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.profiles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate_id = self.matches[self.selected_index].candidate_id;
|
||||
let profile = &self.profiles[candidate_id];
|
||||
|
||||
(self.on_confirm)(&profile.id, window, cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.profile_picker
|
||||
.update(cx, |_this, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let profile_match = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
profile_match.string.clone(),
|
||||
profile_match.positions.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,316 +0,0 @@
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{App, Entity, SharedString};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelRequestMessage, MessageContent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use text::BufferId;
|
||||
use ui::IconName;
|
||||
use util::post_inc;
|
||||
|
||||
use crate::{context_store::buffer_path_log_err, thread::Thread};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct ContextId(pub(crate) usize);
|
||||
|
||||
impl ContextId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
Self(post_inc(&mut self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some context attached to a message in a thread.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextSnapshot {
|
||||
pub id: ContextId,
|
||||
pub name: SharedString,
|
||||
pub parent: Option<SharedString>,
|
||||
pub tooltip: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub kind: ContextKind,
|
||||
/// Joining these strings separated by \n yields text for model. Not refreshed by `snapshot`.
|
||||
pub text: Box<[SharedString]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
Directory,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
|
||||
impl ContextKind {
|
||||
pub fn icon(&self) -> IconName {
|
||||
match self {
|
||||
ContextKind::File => IconName::File,
|
||||
ContextKind::Directory => IconName::Folder,
|
||||
ContextKind::FetchedUrl => IconName::Globe,
|
||||
ContextKind::Thread => IconName::MessageCircle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AssistantContext {
|
||||
File(FileContext),
|
||||
Directory(DirectoryContext),
|
||||
FetchedUrl(FetchedUrlContext),
|
||||
Thread(ThreadContext),
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
pub fn id(&self) -> ContextId {
|
||||
match self {
|
||||
Self::File(file) => file.id,
|
||||
Self::Directory(directory) => directory.snapshot.id,
|
||||
Self::FetchedUrl(url) => url.id,
|
||||
Self::Thread(thread) => thread.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileContext {
|
||||
pub id: ContextId,
|
||||
pub context_buffer: ContextBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryContext {
|
||||
pub path: Rc<Path>,
|
||||
pub context_buffers: Vec<ContextBuffer>,
|
||||
pub snapshot: ContextSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FetchedUrlContext {
|
||||
pub id: ContextId,
|
||||
pub url: SharedString,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
// TODO: Model<Thread> holds onto the thread even if the thread is deleted. Can either handle this
|
||||
// explicitly or have a WeakModel<Thread> and remove during snapshot.
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThreadContext {
|
||||
pub id: ContextId,
|
||||
pub thread: Entity<Thread>,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
// TODO: Model<Buffer> holds onto the buffer even if the file is deleted and closed. Should remove
|
||||
// the context from the message editor in this case.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContextBuffer {
|
||||
pub id: BufferId,
|
||||
pub buffer: Entity<Buffer>,
|
||||
pub version: clock::Global,
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl AssistantContext {
|
||||
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||
match &self {
|
||||
Self::File(file_context) => file_context.snapshot(cx),
|
||||
Self::Directory(directory_context) => Some(directory_context.snapshot()),
|
||||
Self::FetchedUrl(fetched_url_context) => Some(fetched_url_context.snapshot()),
|
||||
Self::Thread(thread_context) => Some(thread_context.snapshot(cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileContext {
|
||||
pub fn snapshot(&self, cx: &App) -> Option<ContextSnapshot> {
|
||||
let buffer = self.context_buffer.buffer.read(cx);
|
||||
let path = buffer_path_log_err(buffer)?;
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
let icon_path = FileIcons::get_icon(&path, cx);
|
||||
|
||||
Some(ContextSnapshot {
|
||||
id: self.id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
icon_path,
|
||||
kind: ContextKind::File,
|
||||
text: Box::new([self.context_buffer.text.clone()]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryContext {
|
||||
pub fn new(
|
||||
id: ContextId,
|
||||
path: &Path,
|
||||
context_buffers: Vec<ContextBuffer>,
|
||||
) -> DirectoryContext {
|
||||
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||
|
||||
let name = match path.file_name() {
|
||||
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||
None => full_path.clone(),
|
||||
};
|
||||
|
||||
let parent = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.map(|p| p.to_string_lossy().into_owned().into());
|
||||
|
||||
// TODO: include directory path in text?
|
||||
let text = context_buffers
|
||||
.iter()
|
||||
.map(|b| b.text.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.into();
|
||||
|
||||
DirectoryContext {
|
||||
path: path.into(),
|
||||
context_buffers,
|
||||
snapshot: ContextSnapshot {
|
||||
id,
|
||||
name,
|
||||
parent,
|
||||
tooltip: Some(full_path),
|
||||
icon_path: None,
|
||||
kind: ContextKind::Directory,
|
||||
text,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> ContextSnapshot {
|
||||
self.snapshot.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FetchedUrlContext {
|
||||
pub fn snapshot(&self) -> ContextSnapshot {
|
||||
ContextSnapshot {
|
||||
id: self.id,
|
||||
name: self.url.clone(),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
kind: ContextKind::FetchedUrl,
|
||||
text: Box::new([self.text.clone()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadContext {
|
||||
pub fn snapshot(&self, cx: &App) -> ContextSnapshot {
|
||||
let thread = self.thread.read(cx);
|
||||
ContextSnapshot {
|
||||
id: self.id,
|
||||
name: thread.summary().unwrap_or("New thread".into()),
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
kind: ContextKind::Thread,
|
||||
text: Box::new([self.text.clone()]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attach_context_to_message(
|
||||
message: &mut LanguageModelRequestMessage,
|
||||
contexts: impl Iterator<Item = ContextSnapshot>,
|
||||
) {
|
||||
let mut file_context = Vec::new();
|
||||
let mut directory_context = Vec::new();
|
||||
let mut fetch_context = Vec::new();
|
||||
let mut thread_context = Vec::new();
|
||||
|
||||
let mut capacity = 0;
|
||||
for context in contexts {
|
||||
capacity += context.text.len();
|
||||
match context.kind {
|
||||
ContextKind::File => file_context.push(context),
|
||||
ContextKind::Directory => directory_context.push(context),
|
||||
ContextKind::FetchedUrl => fetch_context.push(context),
|
||||
ContextKind::Thread => thread_context.push(context),
|
||||
}
|
||||
}
|
||||
if !file_context.is_empty() {
|
||||
capacity += 1;
|
||||
}
|
||||
if !directory_context.is_empty() {
|
||||
capacity += 1;
|
||||
}
|
||||
if !fetch_context.is_empty() {
|
||||
capacity += 1 + fetch_context.len();
|
||||
}
|
||||
if !thread_context.is_empty() {
|
||||
capacity += 1 + thread_context.len();
|
||||
}
|
||||
if capacity == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut context_chunks = Vec::with_capacity(capacity);
|
||||
|
||||
if !file_context.is_empty() {
|
||||
context_chunks.push("The following files are available:\n");
|
||||
for context in &file_context {
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !directory_context.is_empty() {
|
||||
context_chunks.push("The following directories are available:\n");
|
||||
for context in &directory_context {
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !fetch_context.is_empty() {
|
||||
context_chunks.push("The following fetched results are available:\n");
|
||||
for context in &fetch_context {
|
||||
context_chunks.push(&context.name);
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !thread_context.is_empty() {
|
||||
context_chunks.push("The following previous conversation threads are available:\n");
|
||||
for context in &thread_context {
|
||||
context_chunks.push(&context.name);
|
||||
for chunk in &context.text {
|
||||
context_chunks.push(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
context_chunks.len() == capacity,
|
||||
"attach_context_message calculated capacity of {}, but length was {}",
|
||||
capacity,
|
||||
context_chunks.len()
|
||||
);
|
||||
|
||||
if !context_chunks.is_empty() {
|
||||
message
|
||||
.content
|
||||
.push(MessageContent::Text(context_chunks.join("\n")));
|
||||
}
|
||||
}
|
||||
@@ -1,686 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::HashSet;
|
||||
use editor::actions::MoveUp;
|
||||
use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
|
||||
use fs::Fs;
|
||||
use git::ExpandCommitEditor;
|
||||
use git_ui::git_panel;
|
||||
use gpui::{
|
||||
point, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
|
||||
WeakEntity,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, KeyBinding, PlatformStyle, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
};
|
||||
use vim_mode_setting::VimModeSetting;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Entity<Thread>,
|
||||
editor: Entity<Editor>,
|
||||
#[allow(dead_code)]
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
context_store: Entity<ContextStore>,
|
||||
context_strip: Entity<ContextStrip>,
|
||||
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
inline_context_picker: Entity<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
context_store: Entity<ContextStore>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::auto_height(10, window, cx);
|
||||
editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
min_entries_visible: 12,
|
||||
max_entries_visible: 12,
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let editor_entity = editor.downgrade();
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
|
||||
workspace.clone(),
|
||||
context_store.downgrade(),
|
||||
Some(thread_store.clone()),
|
||||
editor_entity,
|
||||
))));
|
||||
});
|
||||
|
||||
let inline_context_picker = cx.new(|cx| {
|
||||
ContextPicker::new(
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_store.downgrade(),
|
||||
ConfirmBehavior::Close,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let context_strip = cx.new(|cx| {
|
||||
ContextStrip::new(
|
||||
context_store.clone(),
|
||||
workspace.clone(),
|
||||
Some(thread_store.clone()),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::File,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(
|
||||
&inline_context_picker,
|
||||
window,
|
||||
Self::handle_inline_context_picker_event,
|
||||
),
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||
];
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
thread,
|
||||
workspace,
|
||||
context_store,
|
||||
context_strip,
|
||||
context_picker_menu_handle,
|
||||
inline_context_picker,
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn toggle_context_picker(
|
||||
&mut self,
|
||||
_: &ToggleContextPicker,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
pub fn remove_all_context(
|
||||
&mut self,
|
||||
_: &RemoveAllContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.context_store.update(cx, |store, _cx| store.clear());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.is_editor_empty(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.thread.read(cx).is_generating() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.send_to_model(RequestKind::Chat, window, cx);
|
||||
}
|
||||
|
||||
fn is_editor_empty(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).text(cx).is_empty()
|
||||
}
|
||||
|
||||
fn is_model_selected(&self, cx: &App) -> bool {
|
||||
LanguageModelRegistry::read_global(cx)
|
||||
.active_model()
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn send_to_model(
|
||||
&mut self,
|
||||
request_kind: RequestKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
{
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(model) = model_registry.active_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let user_message = self.editor.update(cx, |editor, cx| {
|
||||
let text = editor.text(cx);
|
||||
editor.clear(window, cx);
|
||||
text
|
||||
});
|
||||
|
||||
let refresh_task =
|
||||
refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
|
||||
|
||||
let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
|
||||
|
||||
let thread = self.thread.clone();
|
||||
let context_store = self.context_store.clone();
|
||||
let checkpoint = self.project.read(cx).git_store().read(cx).checkpoint(cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let checkpoint = checkpoint.await.ok();
|
||||
refresh_task.await;
|
||||
let (system_prompt_context, load_error) = system_prompt_context_task.await;
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_system_prompt_context(system_prompt_context);
|
||||
if let Some(load_error) = load_error {
|
||||
cx.emit(ThreadEvent::ShowError(load_error));
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
|
||||
thread.insert_user_message(user_message, context, checkpoint, cx);
|
||||
thread.send_to_model(model, request_kind, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn handle_inline_context_picker_event(
|
||||
&mut self,
|
||||
_inline_context_picker: &Entity<ContextPicker>,
|
||||
_event: &DismissEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
window.focus(&editor_focus_handle);
|
||||
}
|
||||
|
||||
fn handle_context_strip_event(
|
||||
&mut self,
|
||||
_context_strip: &Entity<ContextStrip>,
|
||||
event: &ContextStripEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ContextStripEvent::PickerDismissed
|
||||
| ContextStripEvent::BlurredEmpty
|
||||
| ContextStripEvent::BlurredDown => {
|
||||
let editor_focus_handle = self.editor.focus_handle(cx);
|
||||
window.focus(&editor_focus_handle);
|
||||
}
|
||||
ContextStripEvent::BlurredUp => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.context_picker_menu_handle.is_deployed()
|
||||
|| self.inline_context_picker_menu_handle.is_deployed()
|
||||
{
|
||||
cx.propagate();
|
||||
} else {
|
||||
self.context_strip.focus_handle(cx).focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for MessageEditor {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
|
||||
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let inline_context_picker = self.inline_context_picker.clone();
|
||||
|
||||
let empty_thread = self.thread.read(cx).is_empty();
|
||||
let is_generating = self.thread.read(cx).is_generating();
|
||||
let is_model_selected = self.is_model_selected(cx);
|
||||
let is_editor_empty = self.is_editor_empty(cx);
|
||||
let submit_label_color = if is_editor_empty {
|
||||
Color::Muted
|
||||
} else {
|
||||
Color::Default
|
||||
};
|
||||
|
||||
let vim_mode_enabled = VimModeSetting::get_global(cx).0;
|
||||
let platform = PlatformStyle::platform();
|
||||
let linux = platform == PlatformStyle::Linux;
|
||||
let windows = platform == PlatformStyle::Windows;
|
||||
let button_width = if linux || windows || vim_mode_enabled {
|
||||
px(82.)
|
||||
} else {
|
||||
px(64.)
|
||||
};
|
||||
|
||||
let project = self.thread.read(cx).project();
|
||||
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
|
||||
repository.read(cx).cached_status().count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let border_color = cx.theme().colors().border;
|
||||
let active_color = cx.theme().colors().element_selected;
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let edit_files_container = || {
|
||||
h_flex()
|
||||
.mx_2()
|
||||
.py_1()
|
||||
.pl_2p5()
|
||||
.pr_1()
|
||||
.bg(bg_edit_files_disclosure)
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.justify_between()
|
||||
.flex_wrap()
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.when(is_generating, |parent| {
|
||||
let focus_handle = self.editor.focus_handle(cx).clone();
|
||||
parent.child(
|
||||
h_flex().py_3().w_full().justify_center().child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.py_1()
|
||||
.bg(editor_bg_color)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_lg()
|
||||
.shadow_md()
|
||||
.gap_1()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(gpui::Transformation::rotate(
|
||||
gpui::percentage(delta),
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Generating…")
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(ui::Divider::vertical())
|
||||
.child(
|
||||
Button::new("cancel-generation", "Cancel")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding(
|
||||
KeyBinding::for_action_in(
|
||||
&editor::actions::Cancel,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.))),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::Cancel,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
changed_files > 0 && !is_generating && !empty_thread,
|
||||
|parent| {
|
||||
parent.child(
|
||||
edit_files_container()
|
||||
.border_b_0()
|
||||
.rounded_t_md()
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: gpui::black().opacity(0.15),
|
||||
offset: point(px(1.), px(-1.)),
|
||||
blur_radius: px(3.),
|
||||
spread_radius: px(0.),
|
||||
}])
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Edits").size(LabelSize::XSmall))
|
||||
.child(div().size_1().rounded_full().bg(border_color))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_files,
|
||||
if changed_files == 1 { "file" } else { "files" }
|
||||
))
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("panel", "Open Git Panel")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&git_panel::ToggleFocus,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_ev, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&git_panel::ToggleFocus)
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("review", "Review Diff")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&git_ui::project_diff::Diff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&git_ui::project_diff::Diff)
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit Changes")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ExpandCommitEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&ExpandCommitEditor)
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(
|
||||
changed_files > 0 && !is_generating && empty_thread,
|
||||
|parent| {
|
||||
parent.child(
|
||||
edit_files_container()
|
||||
.mb_2()
|
||||
.rounded_md()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Consider committing your changes before starting a fresh thread").size(LabelSize::XSmall))
|
||||
.child(div().size_1().rounded_full().bg(border_color))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
changed_files,
|
||||
if changed_files == 1 { "file" } else { "files" }
|
||||
))
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("review", "Review Diff")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&git_ui::project_diff::Diff,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&git_ui::project_diff::Diff)
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("commit", "Commit Changes")
|
||||
.label_size(LabelSize::XSmall)
|
||||
.key_binding({
|
||||
let focus_handle = focus_handle.clone();
|
||||
KeyBinding::for_action_in(
|
||||
&ExpandCommitEditor,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|kb| kb.size(rems_from_px(10.)))
|
||||
})
|
||||
.on_click(|_event, _window, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.dispatch_action(&ExpandCommitEditor)
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::toggle_context_picker))
|
||||
.on_action(cx.listener(Self::remove_all_context))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
.on_action(cx.listener(Self::toggle_chat_mode))
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.bg(editor_bg_color)
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(h_flex().justify_between().child(self.context_strip.clone()))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: editor_bg_color,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
PopoverMenu::new("inline-context-picker")
|
||||
.menu(move |window, cx| {
|
||||
inline_context_picker.update(cx, |this, cx| {
|
||||
this.init(window, cx);
|
||||
});
|
||||
|
||||
Some(inline_context_picker.clone())
|
||||
})
|
||||
.attach(gpui::Corner::TopLeft)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
|
||||
- px(4.0),
|
||||
})
|
||||
.with_handle(self.inline_context_picker_menu_handle.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(h_flex().gap_2().child(self.profile_selector.clone()))
|
||||
.child(
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("submit-message")
|
||||
.width(button_width.into())
|
||||
.style(ButtonStyle::Filled)
|
||||
.disabled(
|
||||
is_editor_empty
|
||||
|| !is_model_selected
|
||||
|| is_generating,
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new("Submit")
|
||||
.size(LabelSize::Small)
|
||||
.color(submit_label_color),
|
||||
)
|
||||
.children(
|
||||
KeyBinding::for_action_in(
|
||||
&Chat,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.map(|binding| {
|
||||
binding
|
||||
.when(vim_mode_enabled, |kb| {
|
||||
kb.size(rems_from_px(12.))
|
||||
})
|
||||
.into_any_element()
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&Chat, window, cx);
|
||||
})
|
||||
.when(is_editor_empty, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Type a message to submit",
|
||||
))
|
||||
})
|
||||
.when(is_generating, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Cancel to submit a new message",
|
||||
))
|
||||
})
|
||||
.when(!is_model_selected, |button| {
|
||||
button.tooltip(Tooltip::text(
|
||||
"Select a model to continue",
|
||||
))
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
|
||||
use indexmap::IndexMap;
|
||||
use settings::{update_settings_file, Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ManageProfiles, ThreadStore};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
profiles: IndexMap::default(),
|
||||
fs,
|
||||
thread_store,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.refresh_profiles(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.profiles = settings.profiles.clone();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let icon_position = IconPosition::Start;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(&profile_id, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Configure Profiles")
|
||||
.icon(IconName::Pencil)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(ManageProfiles.boxed_clone(), cx);
|
||||
}),
|
||||
);
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile = settings
|
||||
.profiles
|
||||
.get(&settings.default_profile)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let this = cx.entity().clone();
|
||||
PopoverMenu::new("tool-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
Button::new("profile-selector-button", profile)
|
||||
.style(ButtonStyle::Filled)
|
||||
.label_size(LabelSize::Small),
|
||||
Tooltip::text("Change Profile"),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
}
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
use assistant_context_editor::SavedContextMetadata;
|
||||
use gpui::{
|
||||
uniform_list, App, Entity, FocusHandle, Focusable, ScrollStrategy, UniformListScrollHandle,
|
||||
WeakEntity,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
|
||||
use crate::history_store::{HistoryEntry, HistoryStore};
|
||||
use crate::thread_store::SerializedThreadMetadata;
|
||||
use crate::{AssistantPanel, RemoveSelectedThread};
|
||||
|
||||
pub struct ThreadHistory {
|
||||
focus_handle: FocusHandle,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl ThreadHistory {
|
||||
pub(crate) fn new(
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
assistant_panel,
|
||||
history_store,
|
||||
scroll_handle: UniformListScrollHandle::default(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.entry_count(cx));
|
||||
if count > 0 {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_index(count - 1, window, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index - 1, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(
|
||||
&mut self,
|
||||
_: &menu::SelectNext,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let count = self
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.entry_count(cx));
|
||||
if count > 0 {
|
||||
if self.selected_index == count - 1 {
|
||||
self.set_selected_index(0, window, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index + 1, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = self
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.entry_count(cx));
|
||||
if count > 0 {
|
||||
self.set_selected_index(0, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let count = self
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.entry_count(cx));
|
||||
if count > 0 {
|
||||
self.set_selected_index(count - 1, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, index: usize, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_index = index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(index, ScrollStrategy::Top);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let entries = self.history_store.update(cx, |this, cx| this.entries(cx));
|
||||
|
||||
if let Some(entry) = entries.get(self.selected_index) {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
self.assistant_panel
|
||||
.update(cx, move |this, cx| this.open_thread(&thread.id, window, cx))
|
||||
.ok();
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel
|
||||
.update(cx, move |this, cx| {
|
||||
this.open_saved_prompt_editor(context.path.clone(), window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_selected_thread(
|
||||
&mut self,
|
||||
_: &RemoveSelectedThread,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let entries = self.history_store.update(cx, |this, cx| this.entries(cx));
|
||||
|
||||
if let Some(entry) = entries.get(self.selected_index) {
|
||||
match entry {
|
||||
HistoryEntry::Thread(thread) => {
|
||||
self.assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&thread.id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
HistoryEntry::Context(context) => {
|
||||
self.assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(context.path.clone(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ThreadHistory {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ThreadHistory {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let history_entries = self.history_store.update(cx, |this, cx| this.entries(cx));
|
||||
let selected_index = self.selected_index;
|
||||
|
||||
v_flex()
|
||||
.id("thread-history-container")
|
||||
.key_context("ThreadHistory")
|
||||
.track_focus(&self.focus_handle)
|
||||
.overflow_y_scroll()
|
||||
.size_full()
|
||||
.p_1()
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::remove_selected_thread))
|
||||
.map(|history| {
|
||||
if history_entries.is_empty() {
|
||||
history
|
||||
.justify_center()
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("You don't have any past threads yet.")
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
history.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
"thread-history",
|
||||
history_entries.len(),
|
||||
move |history, range, _window, _cx| {
|
||||
history_entries[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, entry)| {
|
||||
h_flex().w_full().pb_1().child(match entry {
|
||||
HistoryEntry::Thread(thread) => PastThread::new(
|
||||
thread.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
selected_index == index,
|
||||
)
|
||||
.into_any_element(),
|
||||
HistoryEntry::Context(context) => PastContext::new(
|
||||
context.clone(),
|
||||
history.assistant_panel.clone(),
|
||||
selected_index == index,
|
||||
)
|
||||
.into_any_element(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastThread {
|
||||
thread: SerializedThreadMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl PastThread {
|
||||
pub fn new(
|
||||
thread: SerializedThreadMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
assistant_panel,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastThread {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.thread.summary;
|
||||
|
||||
let thread_timestamp = time_format::format_localized_timestamp(
|
||||
OffsetDateTime::from_unix_timestamp(self.thread.updated_at.timestamp()).unwrap(),
|
||||
OffsetDateTime::now_utc(),
|
||||
self.assistant_panel
|
||||
.update(cx, |this, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(SharedString::from(self.thread.id.to_string()))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Thread")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text_disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Delete Thread"))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, _window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_thread(&id, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = self.thread.id.clone();
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread(&id, window, cx).detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PastContext {
|
||||
context: SavedContextMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl PastContext {
|
||||
pub fn new(
|
||||
context: SavedContextMetadata,
|
||||
assistant_panel: WeakEntity<AssistantPanel>,
|
||||
selected: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
context,
|
||||
assistant_panel,
|
||||
selected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PastContext {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let summary = self.context.title;
|
||||
|
||||
let context_timestamp = time_format::format_localized_timestamp(
|
||||
OffsetDateTime::from_unix_timestamp(self.context.mtime.timestamp()).unwrap(),
|
||||
OffsetDateTime::now_utc(),
|
||||
self.assistant_panel
|
||||
.update(cx, |this, _cx| this.local_timezone())
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(SharedString::from(
|
||||
self.context.path.to_string_lossy().to_string(),
|
||||
))
|
||||
.rounded()
|
||||
.toggle_state(self.selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
div()
|
||||
.max_w_4_5()
|
||||
.child(Label::new(summary).size(LabelSize::Small).truncate()),
|
||||
)
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Label::new("Prompt Editor")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(3.))
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().text_disabled),
|
||||
)
|
||||
.child(
|
||||
Label::new(context_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.tooltip(Tooltip::text("Delete Prompt Editor"))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, _window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.delete_context(path.clone(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let path = self.context.path.clone();
|
||||
move |_event, window, cx| {
|
||||
assistant_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.open_saved_prompt_editor(path.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod context_pill;
|
||||
mod tool_ready_pop_up;
|
||||
|
||||
pub use context_pill::*;
|
||||
pub use tool_ready_pop_up::*;
|
||||
@@ -22,6 +22,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -53,7 +54,9 @@ theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
mod context_tests;
|
||||
|
||||
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
@@ -12,17 +12,17 @@ use client::{self, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{future::Shared, FutureExt, StreamExt};
|
||||
use futures::{FutureExt, StreamExt, future::Shared};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
|
||||
Task,
|
||||
};
|
||||
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
|
||||
use language_model::{
|
||||
report_assistant_event, LanguageModel, LanguageModelCacheConfiguration,
|
||||
LanguageModelCompletionEvent, LanguageModelImage, LanguageModelRegistry, LanguageModelRequest,
|
||||
LanguageModelRequestMessage, LanguageModelToolUseId, MaxMonthlySpendReachedError,
|
||||
MessageContent, PaymentRequiredError, Role, StopReason,
|
||||
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
|
||||
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
|
||||
LanguageModelToolUseId, MaxMonthlySpendReachedError, MessageContent, PaymentRequiredError,
|
||||
Role, StopReason, report_assistant_event,
|
||||
};
|
||||
use open_ai::Model as OpenAiModel;
|
||||
use paths::contexts_dir;
|
||||
@@ -31,7 +31,7 @@ use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{max, Ordering},
|
||||
cmp::{Ordering, max},
|
||||
fmt::Debug,
|
||||
iter, mem,
|
||||
ops::Range,
|
||||
@@ -43,7 +43,7 @@ use std::{
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use text::{BufferSnapshot, ToPoint};
|
||||
use ui::IconName;
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
@@ -1272,7 +1272,7 @@ impl AssistantContext {
|
||||
// Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
|
||||
// because otherwise you see in the UI that your empty message has a bunch of tokens already used.
|
||||
let request = self.to_completion_request(RequestType::Chat, cx);
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
let debounce = self.token_count.is_some();
|
||||
@@ -1284,10 +1284,12 @@ impl AssistantContext {
|
||||
.await;
|
||||
}
|
||||
|
||||
let token_count = cx.update(|cx| model.count_tokens(request, cx))?.await?;
|
||||
let token_count = cx
|
||||
.update(|cx| model.model.count_tokens(request, cx))?
|
||||
.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.token_count = Some(token_count);
|
||||
this.start_cache_warming(&model, cx);
|
||||
this.start_cache_warming(&model.model, cx);
|
||||
cx.notify()
|
||||
})
|
||||
}
|
||||
@@ -2304,14 +2306,16 @@ impl AssistantContext {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<MessageAnchor> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let provider = model_registry.active_provider()?;
|
||||
let model = model_registry.active_model()?;
|
||||
let model = model_registry.default_model()?;
|
||||
let last_message_id = self.get_last_valid_message_id(cx)?;
|
||||
|
||||
if !provider.is_authenticated(cx) {
|
||||
if !model.provider.is_authenticated(cx) {
|
||||
log::info!("completion provider has no credentials");
|
||||
return None;
|
||||
}
|
||||
|
||||
let model = model.model;
|
||||
|
||||
// Compute which messages to cache, including the last one.
|
||||
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
|
||||
|
||||
@@ -2940,15 +2944,12 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
pub fn summarize(&mut self, replace_old: bool, cx: &mut Context<Self>) {
|
||||
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
|
||||
return;
|
||||
};
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if replace_old || (self.message_anchors.len() >= 2 && self.summary.is_none()) {
|
||||
if !provider.is_authenticated(cx) {
|
||||
if !model.provider.is_authenticated(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2964,7 +2965,7 @@ impl AssistantContext {
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
let stream = model.stream_completion_text(request, &cx);
|
||||
let stream = model.model.stream_completion_text(request, &cx);
|
||||
let mut messages = stream.await?;
|
||||
|
||||
let mut replaced = !replace_old;
|
||||
|
||||
@@ -14,7 +14,7 @@ use futures::{
|
||||
channel::mpsc,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
use gpui::{prelude::*, App, Entity, SharedString, Task, TestAppContext, WeakEntity};
|
||||
use gpui::{App, Entity, SharedString, Task, TestAppContext, WeakEntity, prelude::*};
|
||||
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
|
||||
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
|
||||
use parking_lot::Mutex;
|
||||
@@ -30,14 +30,14 @@ use std::{
|
||||
ops::Range,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
|
||||
use text::{OffsetRangeExt as _, ReplicaId, ToOffset, network::Network};
|
||||
use ui::{IconName, Window};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
RandomCharIter,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
|
||||
@@ -2,36 +2,37 @@ use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{
|
||||
selections_creases, DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs,
|
||||
FileSlashCommand,
|
||||
DefaultSlashCommand, DocsSlashCommand, DocsSlashCommandArgs, FileSlashCommand,
|
||||
selections_creases,
|
||||
};
|
||||
use client::{proto, zed_urls};
|
||||
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||
use collections::{BTreeSet, HashMap, HashSet, hash_map};
|
||||
use editor::{
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
|
||||
display_map::{
|
||||
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
|
||||
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
|
||||
},
|
||||
scroll::Autoscroll,
|
||||
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, ProposedChangeLocation,
|
||||
ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
|
||||
};
|
||||
use editor::{display_map::CreaseId, FoldPlaceholder};
|
||||
use editor::{FoldPlaceholder, display_map::CreaseId};
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions, div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between,
|
||||
size, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
|
||||
Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
|
||||
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
|
||||
Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage,
|
||||
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
|
||||
WeakEntity,
|
||||
WeakEntity, actions, div, img, impl_internal_actions, percentage, point, prelude::*,
|
||||
pulsating_between, size,
|
||||
};
|
||||
use indexed_docs::IndexedDocsStore;
|
||||
use language::{
|
||||
language_settings::{all_language_settings, SoftWrap},
|
||||
BufferSnapshot, LspAdapterDelegate, ToOffset,
|
||||
language_settings::{SoftWrap, all_language_settings},
|
||||
};
|
||||
use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
@@ -46,33 +47,32 @@ use project::lsp_store::LocalLspAdapterDelegate;
|
||||
use project::{Project, Worktree};
|
||||
use rope::Point;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{any::TypeId, borrow::Cow, cmp, ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use text::SelectionGoal;
|
||||
use ui::{
|
||||
prelude::*, ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor,
|
||||
Tooltip,
|
||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::{maybe, ResultExt};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::searchable::{Direction, SearchableItemHandle};
|
||||
use workspace::{
|
||||
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
item::{self, FollowableItem, Item, ItemHandle},
|
||||
notifications::NotificationId,
|
||||
pane::{self, SaveIntent},
|
||||
searchable::{SearchEvent, SearchableItem},
|
||||
Save, ShowConfiguration, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
slash_command::SlashCommandCompletionProvider, slash_command_picker,
|
||||
ThoughtProcessOutputSection,
|
||||
};
|
||||
use crate::{
|
||||
AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
|
||||
ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
|
||||
MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
|
||||
};
|
||||
use crate::{
|
||||
ThoughtProcessOutputSection, slash_command::SlashCommandCompletionProvider,
|
||||
slash_command_picker,
|
||||
};
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
@@ -384,7 +384,9 @@ impl ContextEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
if provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx))
|
||||
@@ -1560,7 +1562,7 @@ impl ContextEditor {
|
||||
})
|
||||
};
|
||||
let create_block_properties = |message: &Message| BlockProperties {
|
||||
height: 2,
|
||||
height: Some(2),
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Above(
|
||||
buffer
|
||||
@@ -2109,7 +2111,7 @@ impl ContextEditor {
|
||||
let image = render_image.clone();
|
||||
anchor.is_valid(&buffer).then(|| BlockProperties {
|
||||
placement: BlockPlacement::Above(anchor),
|
||||
height: MAX_HEIGHT_IN_LINES,
|
||||
height: Some(MAX_HEIGHT_IN_LINES),
|
||||
style: BlockStyle::Sticky,
|
||||
render: Arc::new(move |cx| {
|
||||
let image_size = size_for_image(
|
||||
@@ -2357,7 +2359,19 @@ impl ContextEditor {
|
||||
.on_click({
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(&ShowConfiguration, window, cx);
|
||||
if cx.has_flag::<Assistant2FeatureFlag>() {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::agent::OpenConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::assistant::ShowConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
};
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -2395,13 +2409,13 @@ impl ContextEditor {
|
||||
None => (ButtonStyle::Filled, None),
|
||||
};
|
||||
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
|
||||
let has_configuration_error = configuration_error(cx).is_some();
|
||||
let needs_to_accept_terms = self.show_accept_terms
|
||||
&& provider
|
||||
&& model
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.must_accept_terms(cx));
|
||||
.map_or(false, |model| model.provider.must_accept_terms(cx));
|
||||
let disabled = has_configuration_error || needs_to_accept_terms;
|
||||
|
||||
ButtonLike::new("send_button")
|
||||
@@ -2454,7 +2468,9 @@ impl ContextEditor {
|
||||
None => (ButtonStyle::Filled, None),
|
||||
};
|
||||
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
|
||||
let has_configuration_error = configuration_error(cx).is_some();
|
||||
let needs_to_accept_terms = self.show_accept_terms
|
||||
@@ -2500,7 +2516,9 @@ impl ContextEditor {
|
||||
}
|
||||
|
||||
fn render_language_model_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let active_model = LanguageModelRegistry::read_global(cx).active_model();
|
||||
let active_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.model);
|
||||
let focus_handle = self.editor().focus_handle(cx).clone();
|
||||
let model_name = match active_model {
|
||||
Some(model) => model.name().0,
|
||||
@@ -3020,7 +3038,9 @@ impl EventEmitter<SearchEvent> for ContextEditor {}
|
||||
|
||||
impl Render for ContextEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let provider = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|default| default.provider);
|
||||
let accept_terms = if self.show_accept_terms {
|
||||
provider.as_ref().and_then(|provider| {
|
||||
provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx)
|
||||
@@ -3413,7 +3433,7 @@ impl ContextEditorToolbarItem {
|
||||
pub fn render_remaining_tokens(
|
||||
context_editor: &Entity<ContextEditor>,
|
||||
cx: &App,
|
||||
) -> Option<impl IntoElement> {
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let context = &context_editor.read(cx).context;
|
||||
|
||||
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
|
||||
@@ -3616,7 +3636,9 @@ enum TokenState {
|
||||
fn token_state(context: &Entity<AssistantContext>, cx: &App) -> Option<TokenState> {
|
||||
const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
|
||||
|
||||
let model = LanguageModelRegistry::read_global(cx).active_model()?;
|
||||
let model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()?
|
||||
.model;
|
||||
let token_count = context.read(cx).token_count()?;
|
||||
let max_token_count = model.max_token_count();
|
||||
|
||||
@@ -3669,16 +3691,16 @@ pub enum ConfigurationError {
|
||||
}
|
||||
|
||||
fn configuration_error(cx: &App) -> Option<ConfigurationError> {
|
||||
let provider = LanguageModelRegistry::read_global(cx).active_provider();
|
||||
let is_authenticated = provider
|
||||
let model = LanguageModelRegistry::read_global(cx).default_model();
|
||||
let is_authenticated = model
|
||||
.as_ref()
|
||||
.map_or(false, |provider| provider.is_authenticated(cx));
|
||||
.map_or(false, |model| model.provider.is_authenticated(cx));
|
||||
|
||||
if provider.is_some() && is_authenticated {
|
||||
if model.is_some() && is_authenticated {
|
||||
return None;
|
||||
}
|
||||
|
||||
if provider.is_none() {
|
||||
if model.is_none() {
|
||||
return Some(ConfigurationError::NoProvider);
|
||||
}
|
||||
|
||||
@@ -3703,6 +3725,18 @@ pub fn humanize_token_count(count: usize) -> String {
|
||||
format!("{}.{}k", thousands, hundreds)
|
||||
}
|
||||
}
|
||||
1_000_000..=9_999_999 => {
|
||||
let millions = count / 1_000_000;
|
||||
let hundred_thousands = (count % 1_000_000 + 50_000) / 100_000;
|
||||
if hundred_thousands == 0 {
|
||||
format!("{}M", millions)
|
||||
} else if hundred_thousands == 10 {
|
||||
format!("{}M", millions + 1)
|
||||
} else {
|
||||
format!("{}.{}M", millions, hundred_thousands)
|
||||
}
|
||||
}
|
||||
10_000_000.. => format!("{}M", (count + 500_000) / 1_000_000),
|
||||
_ => format!("{}k", (count + 500) / 1000),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ use std::sync::Arc;
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use ui::utils::{format_distance_from_now, DateTimeType};
|
||||
use ui::{prelude::*, Avatar, ListItem, ListItemSpacing};
|
||||
use ui::utils::{DateTimeType, format_distance_from_now};
|
||||
use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
|
||||
use workspace::{Item, Workspace};
|
||||
|
||||
use crate::{
|
||||
AssistantPanelDelegate, ContextStore, RemoteContextMetadata, SavedContextMetadata,
|
||||
DEFAULT_TAB_TITLE,
|
||||
AssistantPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -229,10 +229,12 @@ impl PickerDelegate for SavedContextPickerDelegate {
|
||||
.into_any_element(),
|
||||
]
|
||||
} else {
|
||||
vec![Label::new("Shared by host")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element()]
|
||||
vec![
|
||||
Label::new("Shared by host")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.into_any_element(),
|
||||
]
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ use crate::{
|
||||
AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use context_server::ContextServerFactoryRegistry;
|
||||
use context_server::manager::ContextServerManager;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
@@ -104,52 +104,47 @@ impl ContextStore {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this =
|
||||
cx.new(|cx: &mut Context<Self>| {
|
||||
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 mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
let this = cx.new(|cx: &mut Context<Self>| {
|
||||
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 mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
context_server_manager,
|
||||
context_server_slash_command_ids: HashMap::default(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
slash_commands,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(cx, |this, cx| this.reload(cx))?.await.log_err();
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.subscribe(&project, Self::handle_project_event)
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
prompt_builder,
|
||||
};
|
||||
this.handle_project_shared(project.clone(), cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
this
|
||||
})?;
|
||||
|
||||
Ok(this)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use editor::ProposedChangesEditor;
|
||||
use futures::{future, TryFutureExt as _};
|
||||
use futures::{TryFutureExt as _, future};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
|
||||
use language::{AutoindentMode, Buffer, BufferSnapshot};
|
||||
use project::{Project, ProjectPath};
|
||||
@@ -548,7 +548,7 @@ mod tests {
|
||||
use super::*;
|
||||
use gpui::App;
|
||||
use language::{
|
||||
language_settings::AllLanguageSettings, Language, LanguageConfig, LanguageMatcher,
|
||||
Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings,
|
||||
};
|
||||
use settings::SettingsStore;
|
||||
use ui::BorrowAppContext;
|
||||
|
||||
@@ -3,19 +3,19 @@ use anyhow::Result;
|
||||
pub use assistant_slash_command::SlashCommand;
|
||||
use assistant_slash_command::{AfterCompletion, SlashCommandLine, SlashCommandWorkingSet};
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{lsp_store::CompletionDocumentation, CompletionIntent, CompletionSource};
|
||||
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
},
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use gpui::{AnyElement, AnyView, DismissEvent, SharedString, Task, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate, PickerEditorPosition};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
|
||||
use ui::{ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip, prelude::*};
|
||||
|
||||
use crate::context_editor::ContextEditor;
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ name = "assistant_eval"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant2.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
clap.workspace = true
|
||||
@@ -27,14 +27,12 @@ fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
node_runtime.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -42,4 +40,7 @@ serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile.workspace = true
|
||||
util.workspace = true
|
||||
walkdir.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,34 +1,25 @@
|
||||
# Tool Evals
|
||||
|
||||
A framework for evaluating and benchmarking AI assistant performance in the Zed editor.
|
||||
A framework for evaluating and benchmarking the agent panel generations.
|
||||
|
||||
## Overview
|
||||
|
||||
Tool Evals provides a headless environment for running assistants evaluations on code repositories. It automates the process of:
|
||||
|
||||
1. Cloning and setting up test repositories
|
||||
1. Setting up test code and repositories
|
||||
2. Sending prompts to language models
|
||||
3. Allowing the assistant to use tools to modify code
|
||||
4. Collecting metrics on performance
|
||||
4. Collecting metrics on performance and tool usage
|
||||
5. Evaluating results against known good solutions
|
||||
|
||||
## How It Works
|
||||
|
||||
The system consists of several key components:
|
||||
|
||||
- **Eval**: Loads test cases from the evaluation_data directory, clones repos, and executes evaluations
|
||||
- **Eval**: Loads exercises from the zed-ace-framework repository, creates temporary repos, and executes evaluations
|
||||
- **HeadlessAssistant**: Provides a headless environment for running the AI assistant
|
||||
- **Judge**: Compares AI-generated diffs with reference solutions and scores their functional similarity
|
||||
|
||||
The evaluation flow:
|
||||
1. An evaluation is loaded from the evaluation_data directory
|
||||
2. The target repository is cloned and checked out at a specific commit
|
||||
3. A HeadlessAssistant instance is created with the specified language model
|
||||
4. The user prompt is sent to the assistant
|
||||
5. The assistant responds and uses tools to modify code
|
||||
6. Upon completion, a diff is generated from the changes
|
||||
7. Results are saved including the diff, assistant's response, and performance metrics
|
||||
8. If a reference solution exists, a Judge evaluates the similarity of the solution
|
||||
- **Judge**: Evaluates AI-generated solutions against reference implementations and assigns scores
|
||||
- **Templates**: Defines evaluation frameworks for different tasks (Project Creation, Code Modification, Conversational Guidance)
|
||||
|
||||
## Setup Requirements
|
||||
|
||||
@@ -36,6 +27,7 @@ The evaluation flow:
|
||||
|
||||
- Rust and Cargo
|
||||
- Git
|
||||
- Python (for report generation)
|
||||
- Network access to clone repositories
|
||||
- Appropriate API keys for language models and git services (Anthropic, GitHub, etc.)
|
||||
|
||||
@@ -43,35 +35,34 @@ The evaluation flow:
|
||||
|
||||
Ensure you have the required API keys set, either from a dev run of Zed or via these environment variables:
|
||||
- `ZED_ANTHROPIC_API_KEY` for Claude models
|
||||
- `ZED_OPENAI_API_KEY` for OpenAI models
|
||||
- `ZED_GITHUB_API_KEY` for GitHub API (or similar)
|
||||
|
||||
## Usage
|
||||
|
||||
### Running a Single Evaluation
|
||||
|
||||
To run a specific evaluation:
|
||||
|
||||
```bash
|
||||
cargo run -p assistant_eval -- bubbletea-add-set-window-title
|
||||
```
|
||||
|
||||
The arguments are regex patterns for the evaluation names to run, so to run all evaluations that contain `bubbletea`, run:
|
||||
|
||||
```bash
|
||||
cargo run -p assistant_eval -- bubbletea
|
||||
```
|
||||
|
||||
To run all evaluations:
|
||||
### Running Evaluations
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo run -p assistant_eval -- --all
|
||||
|
||||
# Run only specific languages
|
||||
cargo run -p assistant_eval -- --all --languages python,rust
|
||||
|
||||
# Limit concurrent evaluations
|
||||
cargo run -p assistant_eval -- --all --concurrency 5
|
||||
|
||||
# Limit number of exercises per language
|
||||
cargo run -p assistant_eval -- --all --max-exercises-per-language 3
|
||||
```
|
||||
|
||||
## Evaluation Data Structure
|
||||
### Evaluation Template Types
|
||||
|
||||
Each evaluation should be placed in the `evaluation_data` directory with the following structure:
|
||||
The system supports three types of evaluation templates:
|
||||
|
||||
* `prompt.txt`: The user's prompt.
|
||||
* `original.diff`: The `git diff` of the change anticipated for this prompt.
|
||||
* `setup.json`: Information about the repo used for the evaluation.
|
||||
1. **ProjectCreation**: Tests the model's ability to create new implementations from scratch
|
||||
2. **CodeModification**: Tests the model's ability to modify existing code to meet new requirements
|
||||
3. **ConversationalGuidance**: Tests the model's ability to provide guidance without writing code
|
||||
|
||||
### Support Repo
|
||||
|
||||
The [zed-industries/zed-ace-framework](https://github.com/zed-industries/zed-ace-framework) contains the analytics and reporting scripts.
|
||||
|
||||
@@ -6,15 +6,6 @@ fn main() {
|
||||
if cfg!(target_os = "macos") {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
|
||||
|
||||
println!("cargo:rerun-if-env-changed=ZED_BUNDLE");
|
||||
if std::env::var("ZED_BUNDLE").ok().as_deref() == Some("true") {
|
||||
// Find WebRTC.framework in the Frameworks folder when running as part of an application bundle.
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks");
|
||||
} else {
|
||||
// Find WebRTC.framework as a sibling of the executable when running outside of an application bundle.
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
|
||||
}
|
||||
|
||||
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
|
||||
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::git_commands::{run_git, setup_temp_repo};
|
||||
use crate::headless_assistant::{HeadlessAppState, HeadlessAssistant};
|
||||
use anyhow::anyhow;
|
||||
use assistant2::RequestKind;
|
||||
use crate::{get_exercise_language, get_exercise_name, templates_eval::Template};
|
||||
use agent::RequestKind;
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Task};
|
||||
use language_model::{LanguageModel, TokenUsage};
|
||||
@@ -10,19 +12,26 @@ use std::{
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use util::command::new_smol_command;
|
||||
|
||||
pub struct Eval {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub repo_path: PathBuf,
|
||||
pub eval_setup: EvalSetup,
|
||||
pub user_prompt: String,
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct EvalResult {
|
||||
pub exercise_name: String,
|
||||
pub template_name: String,
|
||||
pub score: String,
|
||||
pub diff: String,
|
||||
pub assistant_response: String,
|
||||
pub elapsed_time_ms: u128,
|
||||
pub timestamp: u128,
|
||||
// Token usage fields
|
||||
pub input_tokens: usize,
|
||||
pub output_tokens: usize,
|
||||
pub total_tokens: usize,
|
||||
pub tool_use_counts: usize,
|
||||
pub judge_model_name: String, // Added field for judge model name
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct EvalOutput {
|
||||
pub diff: String,
|
||||
pub last_message: String,
|
||||
@@ -38,19 +47,31 @@ pub struct EvalSetup {
|
||||
pub base_sha: String,
|
||||
}
|
||||
|
||||
pub struct Eval {
|
||||
pub repo_path: PathBuf,
|
||||
pub eval_setup: EvalSetup,
|
||||
pub user_prompt: String,
|
||||
}
|
||||
|
||||
impl Eval {
|
||||
/// Loads the eval from a path (typically in `evaluation_data`). Clones and checks out the repo
|
||||
/// if necessary.
|
||||
pub async fn load(name: String, path: PathBuf, repos_dir: &Path) -> anyhow::Result<Self> {
|
||||
// Keep this method for potential future use, but mark it as intentionally unused
|
||||
#[allow(dead_code)]
|
||||
pub async fn load(_name: String, path: PathBuf, repos_dir: &Path) -> Result<Self> {
|
||||
let prompt_path = path.join("prompt.txt");
|
||||
let user_prompt = smol::unblock(|| std::fs::read_to_string(prompt_path)).await?;
|
||||
let setup_path = path.join("setup.json");
|
||||
let setup_contents = smol::unblock(|| std::fs::read_to_string(setup_path)).await?;
|
||||
let eval_setup = serde_json_lenient::from_str_lenient::<EvalSetup>(&setup_contents)?;
|
||||
|
||||
// Move this internal function inside the load method since it's only used here
|
||||
fn repo_dir_name(url: &str) -> String {
|
||||
url.trim_start_matches("https://")
|
||||
.replace(|c: char| !c.is_alphanumeric(), "_")
|
||||
}
|
||||
|
||||
let repo_path = repos_dir.join(repo_dir_name(&eval_setup.url));
|
||||
|
||||
Ok(Eval {
|
||||
name,
|
||||
path,
|
||||
repo_path,
|
||||
eval_setup,
|
||||
user_prompt,
|
||||
@@ -62,9 +83,9 @@ impl Eval {
|
||||
app_state: Arc<HeadlessAppState>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<EvalOutput>> {
|
||||
) -> Task<Result<EvalOutput>> {
|
||||
cx.spawn(async move |cx| {
|
||||
checkout_repo(&self.eval_setup, &self.repo_path).await?;
|
||||
run_git(&self.repo_path, &["checkout", &self.eval_setup.base_sha]).await?;
|
||||
|
||||
let (assistant, done_rx) =
|
||||
cx.update(|cx| HeadlessAssistant::new(app_state.clone(), cx))??;
|
||||
@@ -104,9 +125,43 @@ impl Eval {
|
||||
|
||||
done_rx.recv().await??;
|
||||
|
||||
// Add this section to check untracked files
|
||||
println!("Checking for untracked files:");
|
||||
let untracked = run_git(
|
||||
&self.repo_path,
|
||||
&["ls-files", "--others", "--exclude-standard"],
|
||||
)
|
||||
.await?;
|
||||
if untracked.is_empty() {
|
||||
println!("No untracked files found");
|
||||
} else {
|
||||
// Add all files to git so they appear in the diff
|
||||
println!("Adding untracked files to git");
|
||||
run_git(&self.repo_path, &["add", "."]).await?;
|
||||
}
|
||||
|
||||
// get git status
|
||||
let _status = run_git(&self.repo_path, &["status", "--short"]).await?;
|
||||
|
||||
let elapsed_time = start_time.elapsed()?;
|
||||
|
||||
let diff = query_git(&self.repo_path, vec!["diff"]).await?;
|
||||
// Get diff of staged changes (the files we just added)
|
||||
let staged_diff = run_git(&self.repo_path, &["diff", "--staged"]).await?;
|
||||
|
||||
// Get diff of unstaged changes
|
||||
let unstaged_diff = run_git(&self.repo_path, &["diff"]).await?;
|
||||
|
||||
// Combine both diffs
|
||||
let diff = if unstaged_diff.is_empty() {
|
||||
staged_diff
|
||||
} else if staged_diff.is_empty() {
|
||||
unstaged_diff
|
||||
} else {
|
||||
format!(
|
||||
"# Staged changes\n{}\n\n# Unstaged changes\n{}",
|
||||
staged_diff, unstaged_diff
|
||||
)
|
||||
};
|
||||
|
||||
assistant.update(cx, |assistant, cx| {
|
||||
let thread = assistant.thread.read(cx);
|
||||
@@ -132,12 +187,9 @@ impl Eval {
|
||||
}
|
||||
|
||||
impl EvalOutput {
|
||||
// Method to save the output to a directory
|
||||
pub fn save_to_directory(
|
||||
&self,
|
||||
output_dir: &Path,
|
||||
eval_output_value: String,
|
||||
) -> anyhow::Result<()> {
|
||||
// Keep this method for potential future use, but mark it as intentionally unused
|
||||
#[allow(dead_code)]
|
||||
pub fn save_to_directory(&self, output_dir: &Path, eval_output_value: String) -> Result<()> {
|
||||
// Create the output directory if it doesn't exist
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
@@ -192,76 +244,305 @@ impl EvalOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_dir_name(url: &str) -> String {
|
||||
url.trim_start_matches("https://")
|
||||
.replace(|c: char| !c.is_alphanumeric(), "_")
|
||||
pub async fn read_instructions(exercise_path: &Path) -> Result<String> {
|
||||
let instructions_path = exercise_path.join(".docs").join("instructions.md");
|
||||
println!("Reading instructions from: {}", instructions_path.display());
|
||||
let instructions = smol::unblock(move || std::fs::read_to_string(&instructions_path)).await?;
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
async fn checkout_repo(eval_setup: &EvalSetup, repo_path: &Path) -> anyhow::Result<()> {
|
||||
if !repo_path.exists() {
|
||||
smol::unblock({
|
||||
let repo_path = repo_path.to_path_buf();
|
||||
|| std::fs::create_dir_all(repo_path)
|
||||
})
|
||||
.await?;
|
||||
run_git(repo_path, vec!["init"]).await?;
|
||||
run_git(repo_path, vec!["remote", "add", "origin", &eval_setup.url]).await?;
|
||||
} else {
|
||||
let actual_origin = query_git(repo_path, vec!["remote", "get-url", "origin"]).await?;
|
||||
if actual_origin != eval_setup.url {
|
||||
return Err(anyhow!(
|
||||
"remote origin {} does not match expected origin {}",
|
||||
actual_origin,
|
||||
eval_setup.url
|
||||
));
|
||||
}
|
||||
pub async fn read_example_solution(exercise_path: &Path, language: &str) -> Result<String> {
|
||||
// Map the language to the file extension
|
||||
let language_extension = match language {
|
||||
"python" => "py",
|
||||
"go" => "go",
|
||||
"rust" => "rs",
|
||||
"typescript" => "ts",
|
||||
"javascript" => "js",
|
||||
"ruby" => "rb",
|
||||
"php" => "php",
|
||||
"bash" => "sh",
|
||||
"multi" => "diff",
|
||||
"internal" => "diff",
|
||||
_ => return Err(anyhow!("Unsupported language: {}", language)),
|
||||
};
|
||||
let example_path = exercise_path
|
||||
.join(".meta")
|
||||
.join(format!("example.{}", language_extension));
|
||||
println!("Reading example solution from: {}", example_path.display());
|
||||
let example = smol::unblock(move || std::fs::read_to_string(&example_path)).await?;
|
||||
Ok(example)
|
||||
}
|
||||
|
||||
// TODO: consider including "-x" to remove ignored files. The downside of this is that it will
|
||||
// also remove build artifacts, and so prevent incremental reuse there.
|
||||
run_git(repo_path, vec!["clean", "--force", "-d"]).await?;
|
||||
run_git(repo_path, vec!["reset", "--hard", "HEAD"]).await?;
|
||||
pub async fn save_eval_results(exercise_path: &Path, results: Vec<EvalResult>) -> Result<()> {
|
||||
let eval_dir = exercise_path.join("evaluation");
|
||||
fs::create_dir_all(&eval_dir)?;
|
||||
|
||||
let eval_file = eval_dir.join("evals.json");
|
||||
|
||||
println!("Saving evaluation results to: {}", eval_file.display());
|
||||
println!(
|
||||
"Results to save: {} evaluations for exercise path: {}",
|
||||
results.len(),
|
||||
exercise_path.display()
|
||||
);
|
||||
|
||||
// Check file existence before reading/writing
|
||||
if eval_file.exists() {
|
||||
println!("Existing evals.json file found, will update it");
|
||||
} else {
|
||||
println!("No existing evals.json file found, will create new one");
|
||||
}
|
||||
|
||||
run_git(
|
||||
repo_path,
|
||||
vec!["fetch", "--depth", "1", "origin", &eval_setup.base_sha],
|
||||
)
|
||||
.await?;
|
||||
run_git(repo_path, vec!["checkout", &eval_setup.base_sha]).await?;
|
||||
// Structure to organize evaluations by test name and timestamp
|
||||
let mut eval_data: serde_json::Value = if eval_file.exists() {
|
||||
let content = fs::read_to_string(&eval_file)?;
|
||||
serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
};
|
||||
|
||||
// Get current timestamp for this batch of results
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_millis()
|
||||
.to_string();
|
||||
|
||||
// Group the new results by test name (exercise name)
|
||||
for result in results {
|
||||
let exercise_name = &result.exercise_name;
|
||||
let template_name = &result.template_name;
|
||||
|
||||
println!(
|
||||
"Adding result: exercise={}, template={}",
|
||||
exercise_name, template_name
|
||||
);
|
||||
|
||||
// Ensure the exercise entry exists
|
||||
if eval_data.get(exercise_name).is_none() {
|
||||
eval_data[exercise_name] = serde_json::json!({});
|
||||
}
|
||||
|
||||
// Ensure the timestamp entry exists as an object
|
||||
if eval_data[exercise_name].get(×tamp).is_none() {
|
||||
eval_data[exercise_name][×tamp] = serde_json::json!({});
|
||||
}
|
||||
|
||||
// Add this result under the timestamp with template name as key
|
||||
eval_data[exercise_name][×tamp][template_name] = serde_json::to_value(&result)?;
|
||||
}
|
||||
|
||||
// Write back to file with pretty formatting
|
||||
let json_content = serde_json::to_string_pretty(&eval_data)?;
|
||||
match fs::write(&eval_file, json_content) {
|
||||
Ok(_) => println!("✓ Successfully saved results to {}", eval_file.display()),
|
||||
Err(e) => println!("✗ Failed to write results file: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_git(repo_path: &Path, args: Vec<&str>) -> anyhow::Result<()> {
|
||||
let exit_status = new_smol_command("git")
|
||||
.current_dir(repo_path)
|
||||
.args(args.clone())
|
||||
.status()
|
||||
.await?;
|
||||
if exit_status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"`git {}` failed with {}",
|
||||
args.join(" "),
|
||||
exit_status,
|
||||
))
|
||||
}
|
||||
}
|
||||
pub async fn run_exercise_eval(
|
||||
exercise_path: PathBuf,
|
||||
template: Template,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
judge_model: Arc<dyn LanguageModel>,
|
||||
app_state: Arc<HeadlessAppState>,
|
||||
base_sha: String,
|
||||
_framework_path: PathBuf,
|
||||
cx: gpui::AsyncApp,
|
||||
) -> Result<EvalResult> {
|
||||
let exercise_name = get_exercise_name(&exercise_path);
|
||||
let language = get_exercise_language(&exercise_path)?;
|
||||
let mut instructions = read_instructions(&exercise_path).await?;
|
||||
instructions.push_str(&format!(
|
||||
"\n\nWhen writing the code for this prompt, use {} to achieve the goal.",
|
||||
language
|
||||
));
|
||||
let example_solution = read_example_solution(&exercise_path, &language).await?;
|
||||
|
||||
async fn query_git(repo_path: &Path, args: Vec<&str>) -> anyhow::Result<String> {
|
||||
let output = new_smol_command("git")
|
||||
.current_dir(repo_path)
|
||||
.args(args.clone())
|
||||
.output()
|
||||
println!(
|
||||
"Running evaluation for exercise: {} with template: {}",
|
||||
exercise_name, template.name
|
||||
);
|
||||
|
||||
// Create temporary directory with exercise files
|
||||
let temp_dir = setup_temp_repo(&exercise_path, &base_sha).await?;
|
||||
let temp_path = temp_dir.path().to_path_buf();
|
||||
|
||||
if template.name == "ProjectCreation" {
|
||||
for entry in fs::read_dir(&temp_path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
// Skip directories that start with dot (like .docs, .meta, .git)
|
||||
if path.is_dir()
|
||||
&& path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.starts_with("."))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete regular files
|
||||
if path.is_file() {
|
||||
println!(" Deleting file: {}", path.display());
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit the deletion so it shows up in the diff
|
||||
run_git(&temp_path, &["add", "."]).await?;
|
||||
run_git(
|
||||
&temp_path,
|
||||
&["commit", "-m", "Remove root files for clean slate"],
|
||||
)
|
||||
.await?;
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"`git {}` failed with {}",
|
||||
args.join(" "),
|
||||
output.status
|
||||
))
|
||||
}
|
||||
|
||||
let local_commit_sha = run_git(&temp_path, &["rev-parse", "HEAD"]).await?;
|
||||
|
||||
// Prepare prompt based on template
|
||||
let prompt = match template.name {
|
||||
"ProjectCreation" => format!(
|
||||
"I need to create a new implementation for this exercise. Please create all the necessary files in the best location.\n\n{}",
|
||||
instructions
|
||||
),
|
||||
"CodeModification" => format!(
|
||||
"I need help updating my code to meet these requirements. Please modify the appropriate files:\n\n{}",
|
||||
instructions
|
||||
),
|
||||
"ConversationalGuidance" => format!(
|
||||
"I'm trying to solve this coding exercise but I'm not sure where to start. Can you help me understand the requirements and guide me through the solution process without writing code for me?\n\n{}",
|
||||
instructions
|
||||
),
|
||||
_ => instructions.clone(),
|
||||
};
|
||||
|
||||
let start_time = SystemTime::now();
|
||||
|
||||
// Create a basic eval struct to work with the existing system
|
||||
let eval = Eval {
|
||||
repo_path: temp_path.clone(),
|
||||
eval_setup: EvalSetup {
|
||||
url: format!("file://{}", temp_path.display()),
|
||||
base_sha: local_commit_sha, // Use the local commit SHA instead of the framework base SHA
|
||||
},
|
||||
user_prompt: prompt,
|
||||
};
|
||||
|
||||
// Run the evaluation
|
||||
let eval_output = cx
|
||||
.update(|cx| eval.run(app_state.clone(), model.clone(), cx))?
|
||||
.await?;
|
||||
|
||||
// Get diff from git
|
||||
let diff = eval_output.diff.clone();
|
||||
|
||||
// For project creation template, we need to compare with reference implementation
|
||||
let judge_output = if template.name == "ProjectCreation" {
|
||||
let project_judge_prompt = template
|
||||
.content
|
||||
.replace(
|
||||
"<!-- ```requirements go here``` -->",
|
||||
&format!("```\n{}\n```", instructions),
|
||||
)
|
||||
.replace(
|
||||
"<!-- ```reference code goes here``` -->",
|
||||
&format!("```{}\n{}\n```", language, example_solution),
|
||||
)
|
||||
.replace(
|
||||
"<!-- ```git diff goes here``` -->",
|
||||
&format!("```\n{}\n```", diff),
|
||||
);
|
||||
|
||||
// Use the run_with_prompt method which we'll add to judge.rs
|
||||
let judge = crate::judge::Judge {
|
||||
original_diff: None,
|
||||
original_message: Some(project_judge_prompt),
|
||||
model: judge_model.clone(),
|
||||
};
|
||||
|
||||
cx.update(|cx| judge.run_with_prompt(cx))?.await?
|
||||
} else if template.name == "CodeModification" {
|
||||
// For CodeModification, we'll compare the example solution with the LLM-generated solution
|
||||
let code_judge_prompt = template
|
||||
.content
|
||||
.replace(
|
||||
"<!-- ```reference code goes here``` -->",
|
||||
&format!("```{}\n{}\n```", language, example_solution),
|
||||
)
|
||||
.replace(
|
||||
"<!-- ```git diff goes here``` -->",
|
||||
&format!("```\n{}\n```", diff),
|
||||
);
|
||||
|
||||
// Use the run_with_prompt method
|
||||
let judge = crate::judge::Judge {
|
||||
original_diff: None,
|
||||
original_message: Some(code_judge_prompt),
|
||||
model: judge_model.clone(),
|
||||
};
|
||||
|
||||
cx.update(|cx| judge.run_with_prompt(cx))?.await?
|
||||
} else {
|
||||
// Conversational template
|
||||
let conv_judge_prompt = template
|
||||
.content
|
||||
.replace(
|
||||
"<!-- ```query goes here``` -->",
|
||||
&format!("```\n{}\n```", instructions),
|
||||
)
|
||||
.replace(
|
||||
"<!-- ```transcript goes here``` -->",
|
||||
&format!("```\n{}\n```", eval_output.last_message),
|
||||
)
|
||||
.replace(
|
||||
"<!-- ```git diff goes here``` -->",
|
||||
&format!("```\n{}\n```", diff),
|
||||
);
|
||||
|
||||
// Use the run_with_prompt method for consistency
|
||||
let judge = crate::judge::Judge {
|
||||
original_diff: None,
|
||||
original_message: Some(conv_judge_prompt),
|
||||
model: judge_model.clone(),
|
||||
};
|
||||
|
||||
cx.update(|cx| judge.run_with_prompt(cx))?.await?
|
||||
};
|
||||
|
||||
let elapsed_time = start_time.elapsed()?;
|
||||
|
||||
// Calculate total tokens as the sum of input and output tokens
|
||||
let input_tokens = eval_output.token_usage.input_tokens;
|
||||
let output_tokens = eval_output.token_usage.output_tokens;
|
||||
let tool_use_counts = eval_output.tool_use_counts.values().sum::<u32>();
|
||||
let total_tokens = input_tokens + output_tokens;
|
||||
|
||||
// Get judge model name
|
||||
let judge_model_name = judge_model.id().0.to_string();
|
||||
|
||||
// Save results to evaluation directory
|
||||
let result = EvalResult {
|
||||
exercise_name: exercise_name.clone(),
|
||||
template_name: template.name.to_string(),
|
||||
score: judge_output.trim().to_string(),
|
||||
diff,
|
||||
assistant_response: eval_output.last_message.clone(),
|
||||
elapsed_time_ms: elapsed_time.as_millis(),
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)?
|
||||
.as_millis(),
|
||||
// Convert u32 token counts to usize
|
||||
input_tokens: input_tokens.try_into().unwrap(),
|
||||
output_tokens: output_tokens.try_into().unwrap(),
|
||||
total_tokens: total_tokens.try_into().unwrap(),
|
||||
tool_use_counts: tool_use_counts.try_into().unwrap(),
|
||||
judge_model_name, // Add judge model name to result
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
149
crates/assistant_eval/src/get_exercise.rs
Normal file
149
crates/assistant_eval/src/get_exercise.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub fn get_exercise_name(exercise_path: &Path) -> String {
|
||||
exercise_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn get_exercise_language(exercise_path: &Path) -> Result<String> {
|
||||
// Extract the language from path (data/python/exercises/... => python)
|
||||
let parts: Vec<_> = exercise_path.components().collect();
|
||||
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
if i > 0 && part.as_os_str() == "eval_code" {
|
||||
if i + 1 < parts.len() {
|
||||
let language = parts[i + 1].as_os_str().to_string_lossy().to_string();
|
||||
return Ok(language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"Could not determine language from path: {:?}",
|
||||
exercise_path
|
||||
))
|
||||
}
|
||||
|
||||
pub fn find_exercises(
|
||||
framework_path: &Path,
|
||||
languages: &[&str],
|
||||
max_per_language: Option<usize>,
|
||||
) -> Result<Vec<PathBuf>> {
|
||||
let mut all_exercises = Vec::new();
|
||||
|
||||
println!("Searching for exercises in languages: {:?}", languages);
|
||||
|
||||
for language in languages {
|
||||
let language_dir = framework_path
|
||||
.join("eval_code")
|
||||
.join(language)
|
||||
.join("exercises")
|
||||
.join("practice");
|
||||
|
||||
println!("Checking language directory: {:?}", language_dir);
|
||||
if !language_dir.exists() {
|
||||
println!("Warning: Language directory not found: {:?}", language_dir);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut exercises = Vec::new();
|
||||
match fs::read_dir(&language_dir) {
|
||||
Ok(entries) => {
|
||||
for entry_result in entries {
|
||||
match entry_result {
|
||||
Ok(entry) => {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
// Special handling for "internal" directory
|
||||
if *language == "internal" {
|
||||
// Check for repo_info.json to validate it's an internal exercise
|
||||
let repo_info_path = path.join(".meta").join("repo_info.json");
|
||||
let instructions_path =
|
||||
path.join(".docs").join("instructions.md");
|
||||
|
||||
if repo_info_path.exists() && instructions_path.exists() {
|
||||
exercises.push(path);
|
||||
}
|
||||
} else {
|
||||
// Map the language to the file extension - original code
|
||||
let language_extension = match *language {
|
||||
"python" => "py",
|
||||
"go" => "go",
|
||||
"rust" => "rs",
|
||||
"typescript" => "ts",
|
||||
"javascript" => "js",
|
||||
"ruby" => "rb",
|
||||
"php" => "php",
|
||||
"bash" => "sh",
|
||||
"multi" => "diff",
|
||||
_ => continue, // Skip unsupported languages
|
||||
};
|
||||
|
||||
// Check if this is a valid exercise with instructions and example
|
||||
let instructions_path =
|
||||
path.join(".docs").join("instructions.md");
|
||||
let has_instructions = instructions_path.exists();
|
||||
let example_path = path
|
||||
.join(".meta")
|
||||
.join(format!("example.{}", language_extension));
|
||||
let has_example = example_path.exists();
|
||||
|
||||
if has_instructions && has_example {
|
||||
exercises.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => println!("Error reading directory entry: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => println!(
|
||||
"Error reading directory {}: {}",
|
||||
language_dir.display(),
|
||||
err
|
||||
),
|
||||
}
|
||||
|
||||
// Sort exercises by name for consistent selection
|
||||
exercises.sort_by(|a, b| {
|
||||
let a_name = a.file_name().unwrap_or_default().to_string_lossy();
|
||||
let b_name = b.file_name().unwrap_or_default().to_string_lossy();
|
||||
a_name.cmp(&b_name)
|
||||
});
|
||||
|
||||
// Apply the limit if specified
|
||||
if let Some(limit) = max_per_language {
|
||||
if exercises.len() > limit {
|
||||
println!(
|
||||
"Limiting {} exercises to {} for language {}",
|
||||
exercises.len(),
|
||||
limit,
|
||||
language
|
||||
);
|
||||
exercises.truncate(limit);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"Found {} exercises for language {}: {:?}",
|
||||
exercises.len(),
|
||||
language,
|
||||
exercises
|
||||
.iter()
|
||||
.map(|p| p.file_name().unwrap_or_default().to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
all_exercises.extend(exercises);
|
||||
}
|
||||
|
||||
Ok(all_exercises)
|
||||
}
|
||||
125
crates/assistant_eval/src/git_commands.rs
Normal file
125
crates/assistant_eval/src/git_commands.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use serde::Deserialize;
|
||||
use std::{fs, path::Path};
|
||||
use tempfile::TempDir;
|
||||
use util::command::new_smol_command;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetupConfig {
|
||||
#[serde(rename = "base.sha")]
|
||||
pub base_sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RepoInfo {
|
||||
pub remote_url: String,
|
||||
pub head_sha: String,
|
||||
}
|
||||
|
||||
pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result<String> {
|
||||
let output = new_smol_command("git")
|
||||
.current_dir(repo_path)
|
||||
.args(args)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Git command failed: {} with status: {}",
|
||||
args.join(" "),
|
||||
output.status
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_base_sha(framework_path: &Path) -> Result<String> {
|
||||
let setup_path = framework_path.join("setup.json");
|
||||
let setup_content = smol::unblock(move || std::fs::read_to_string(&setup_path)).await?;
|
||||
let setup_config: SetupConfig = serde_json_lenient::from_str_lenient(&setup_content)?;
|
||||
Ok(setup_config.base_sha)
|
||||
}
|
||||
|
||||
pub async fn read_repo_info(exercise_path: &Path) -> Result<RepoInfo> {
|
||||
let repo_info_path = exercise_path.join(".meta").join("repo_info.json");
|
||||
println!("Reading repo info from: {}", repo_info_path.display());
|
||||
let repo_info_content = smol::unblock(move || std::fs::read_to_string(&repo_info_path)).await?;
|
||||
let repo_info: RepoInfo = serde_json_lenient::from_str_lenient(&repo_info_content)?;
|
||||
|
||||
// Remove any quotes from the strings
|
||||
let remote_url = repo_info.remote_url.trim_matches('"').to_string();
|
||||
let head_sha = repo_info.head_sha.trim_matches('"').to_string();
|
||||
|
||||
Ok(RepoInfo {
|
||||
remote_url,
|
||||
head_sha,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_temp_repo(exercise_path: &Path, _base_sha: &str) -> Result<TempDir> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// Check if this is an internal exercise by looking for repo_info.json
|
||||
let repo_info_path = exercise_path.join(".meta").join("repo_info.json");
|
||||
if repo_info_path.exists() {
|
||||
// This is an internal exercise, handle it differently
|
||||
let repo_info = read_repo_info(exercise_path).await?;
|
||||
|
||||
// Clone the repository to the temp directory
|
||||
let url = repo_info.remote_url;
|
||||
let clone_path = temp_dir.path();
|
||||
println!(
|
||||
"Cloning repository from {} to {}",
|
||||
url,
|
||||
clone_path.display()
|
||||
);
|
||||
run_git(
|
||||
&std::env::current_dir()?,
|
||||
&["clone", &url, &clone_path.to_string_lossy()],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Checkout the specified commit
|
||||
println!("Checking out commit: {}", repo_info.head_sha);
|
||||
run_git(temp_dir.path(), &["checkout", &repo_info.head_sha]).await?;
|
||||
|
||||
println!("Successfully set up internal repository");
|
||||
} else {
|
||||
// Original code for regular exercises
|
||||
// Copy the exercise files to the temp directory, excluding .docs and .meta
|
||||
for entry in WalkDir::new(exercise_path).min_depth(0).max_depth(10) {
|
||||
let entry = entry?;
|
||||
let source_path = entry.path();
|
||||
|
||||
// Skip .docs and .meta directories completely
|
||||
if source_path.starts_with(exercise_path.join(".docs"))
|
||||
|| source_path.starts_with(exercise_path.join(".meta"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if source_path.is_file() {
|
||||
let relative_path = source_path.strip_prefix(exercise_path)?;
|
||||
let dest_path = temp_dir.path().join(relative_path);
|
||||
|
||||
// Make sure parent directories exist
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
fs::copy(source_path, dest_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize git repo in the temp directory
|
||||
run_git(temp_dir.path(), &["init"]).await?;
|
||||
run_git(temp_dir.path(), &["add", "."]).await?;
|
||||
run_git(temp_dir.path(), &["commit", "-m", "Initial commit"]).await?;
|
||||
|
||||
println!("Created temp repo without .docs and .meta directories");
|
||||
}
|
||||
|
||||
Ok(temp_dir)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use agent::{RequestKind, Thread, ThreadEvent, ThreadStore};
|
||||
use anyhow::anyhow;
|
||||
use assistant2::{RequestKind, Thread, ThreadEvent, ThreadStore};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{Client, UserStore};
|
||||
use collections::HashMap;
|
||||
use dap::DapRegistry;
|
||||
use futures::StreamExt;
|
||||
use gpui::{prelude::*, App, AsyncApp, Entity, SemanticVersion, Subscription, Task};
|
||||
use gpui::{App, AsyncApp, Entity, SemanticVersion, Subscription, Task, prelude::*};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
AuthenticateError, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
|
||||
@@ -102,6 +102,40 @@ impl HeadlessAssistant {
|
||||
thread.use_pending_tools(cx);
|
||||
});
|
||||
}
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
// Automatically approve all tools that need confirmation in headless mode
|
||||
println!("Tool confirmation needed - automatically approving in headless mode");
|
||||
|
||||
// Get the tools needing confirmation
|
||||
let tools_needing_confirmation: Vec<_> = thread
|
||||
.read(cx)
|
||||
.tools_needing_confirmation()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Run each tool that needs confirmation
|
||||
for tool_use in tools_needing_confirmation {
|
||||
if let Some(tool) = thread.read(cx).tools().tool(&tool_use.name, cx) {
|
||||
thread.update(cx, |thread, cx| {
|
||||
println!("Auto-approving tool: {}", tool_use.name);
|
||||
|
||||
// Create a request to send to the tool
|
||||
let request = thread.to_completion_request(RequestKind::Chat, cx);
|
||||
let messages = Arc::new(request.messages);
|
||||
|
||||
// Run the tool
|
||||
thread.run_tool(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
tool_use.input.clone(),
|
||||
&messages,
|
||||
tool,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
@@ -122,11 +156,15 @@ impl HeadlessAssistant {
|
||||
}
|
||||
if thread.read(cx).all_tools_finished() {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(model) = model_registry.active_model() {
|
||||
if let Some(model) = model_registry.default_model() {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.attach_tool_results(vec![], cx);
|
||||
thread.send_to_model(model, RequestKind::Chat, cx);
|
||||
thread.attach_tool_results(cx);
|
||||
thread.send_to_model(model.model, RequestKind::Chat, cx);
|
||||
});
|
||||
} else {
|
||||
println!(
|
||||
"Warning: No active language model available to continue conversation"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +205,7 @@ pub fn init(cx: &mut App) -> Arc<HeadlessAppState> {
|
||||
context_server::init(cx);
|
||||
let stdout_is_a_pty = false;
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
|
||||
assistant2::init(fs.clone(), client.clone(), prompt_builder.clone(), cx);
|
||||
agent::init(fs.clone(), client.clone(), prompt_builder.clone(), cx);
|
||||
|
||||
Arc::new(HeadlessAppState {
|
||||
languages,
|
||||
|
||||
@@ -1,58 +1,28 @@
|
||||
use crate::eval::EvalOutput;
|
||||
use crate::headless_assistant::send_language_model_request;
|
||||
use anyhow::anyhow;
|
||||
use gpui::{App, Task};
|
||||
use language_model::{
|
||||
LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Judge {
|
||||
pub original_diff: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
pub original_diff: Option<String>,
|
||||
pub original_message: Option<String>,
|
||||
pub model: Arc<dyn LanguageModel>,
|
||||
}
|
||||
|
||||
impl Judge {
|
||||
pub async fn load(eval_path: &Path, model: Arc<dyn LanguageModel>) -> anyhow::Result<Judge> {
|
||||
let original_diff_path = eval_path.join("original.diff");
|
||||
let original_diff = smol::unblock(move || {
|
||||
if std::fs::exists(&original_diff_path)? {
|
||||
anyhow::Ok(Some(std::fs::read_to_string(&original_diff_path)?))
|
||||
} else {
|
||||
anyhow::Ok(None)
|
||||
}
|
||||
});
|
||||
|
||||
let original_message_path = eval_path.join("original_message.txt");
|
||||
let original_message = smol::unblock(move || {
|
||||
if std::fs::exists(&original_message_path)? {
|
||||
anyhow::Ok(Some(std::fs::read_to_string(&original_message_path)?))
|
||||
} else {
|
||||
anyhow::Ok(None)
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
original_diff: original_diff.await?,
|
||||
original_message: original_message.await?,
|
||||
model,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&self, eval_output: &EvalOutput, cx: &mut App) -> Task<anyhow::Result<String>> {
|
||||
let Some(original_diff) = self.original_diff.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("No original.diff found")));
|
||||
pub fn run_with_prompt(&self, cx: &mut App) -> Task<anyhow::Result<String>> {
|
||||
let Some(prompt) = self.original_message.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("No prompt provided in original_message")));
|
||||
};
|
||||
|
||||
// TODO: check for empty diff?
|
||||
let prompt = diff_comparison_prompt(&original_diff, &eval_output.diff);
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text(prompt)],
|
||||
content: vec![MessageContent::Text(prompt.clone())],
|
||||
cache: false,
|
||||
}],
|
||||
temperature: Some(0.0),
|
||||
@@ -61,61 +31,7 @@ impl Judge {
|
||||
};
|
||||
|
||||
let model = self.model.clone();
|
||||
let request = request.clone();
|
||||
cx.spawn(async move |cx| send_language_model_request(model, request, cx).await)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_comparison_prompt(original_diff: &str, new_diff: &str) -> String {
|
||||
format!(
|
||||
r#"# Git Diff Similarity Evaluation Template
|
||||
|
||||
## Instructions
|
||||
|
||||
Compare the two diffs and score them between 0.0 and 1.0 based on their functional similarity.
|
||||
- 1.0 = Perfect functional match (achieves identical results)
|
||||
- 0.0 = No functional similarity whatsoever
|
||||
|
||||
## Evaluation Criteria
|
||||
|
||||
Please consider the following aspects in order of importance:
|
||||
|
||||
1. **Functional Equivalence (60%)**
|
||||
- Do both diffs achieve the same end result?
|
||||
- Are the changes functionally equivalent despite possibly using different approaches?
|
||||
- Do the modifications address the same issues or implement the same features?
|
||||
|
||||
2. **Logical Structure (20%)**
|
||||
- Are the logical flows similar?
|
||||
- Do the modifications affect the same code paths?
|
||||
- Are control structures (if/else, loops, etc.) modified in similar ways?
|
||||
|
||||
3. **Code Content (15%)**
|
||||
- Are similar lines added/removed?
|
||||
- Are the same variables, functions, or methods being modified?
|
||||
- Are the same APIs or libraries being used?
|
||||
|
||||
4. **File Layout (5%)**
|
||||
- Are the same files being modified?
|
||||
- Are changes occurring in similar locations within files?
|
||||
|
||||
## Input
|
||||
|
||||
Original Diff:
|
||||
```git
|
||||
{}
|
||||
```
|
||||
|
||||
New Diff:
|
||||
```git
|
||||
{}
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
|
||||
|
||||
Example output:
|
||||
0.85"#,
|
||||
original_diff, new_diff
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
mod eval;
|
||||
mod get_exercise;
|
||||
mod git_commands;
|
||||
mod headless_assistant;
|
||||
mod judge;
|
||||
mod templates_eval;
|
||||
|
||||
use clap::Parser;
|
||||
use eval::{Eval, EvalOutput};
|
||||
use futures::future;
|
||||
use gpui::{Application, AsyncApp};
|
||||
use headless_assistant::{authenticate_model_provider, find_model, HeadlessAppState};
|
||||
use itertools::Itertools;
|
||||
use judge::Judge;
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use regex::Regex;
|
||||
use eval::{run_exercise_eval, save_eval_results};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use get_exercise::{find_exercises, get_exercise_language, get_exercise_name};
|
||||
use git_commands::read_base_sha;
|
||||
use gpui::Application;
|
||||
use headless_assistant::{authenticate_model_provider, find_model};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use std::{cmp, path::PathBuf, sync::Arc};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use templates_eval::all_templates;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
@@ -21,204 +24,231 @@ use std::{cmp, path::PathBuf, sync::Arc};
|
||||
before_help = "Tool eval runner"
|
||||
)]
|
||||
struct Args {
|
||||
/// Regexes to match the names of evals to run.
|
||||
eval_name_regexes: Vec<String>,
|
||||
/// Runs all evals in `evaluation_data`, causes the regex to be ignored.
|
||||
/// Match the names of evals to run.
|
||||
#[arg(long)]
|
||||
exercise_names: Vec<String>,
|
||||
/// Runs all exercises, causes the exercise_names to be ignored.
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
/// Supported language types to evaluate (default: internal).
|
||||
/// Internal is data generated from the agent panel
|
||||
#[arg(long, default_value = "internal")]
|
||||
languages: String,
|
||||
/// Name of the model (default: "claude-3-7-sonnet-latest")
|
||||
#[arg(long, default_value = "claude-3-7-sonnet-latest")]
|
||||
model_name: String,
|
||||
/// Name of the editor model (default: value of `--model_name`).
|
||||
#[arg(long)]
|
||||
editor_model_name: Option<String>,
|
||||
/// Name of the judge model (default: value of `--model_name`).
|
||||
#[arg(long)]
|
||||
judge_model_name: Option<String>,
|
||||
/// Number of evaluations to run concurrently (default: 10)
|
||||
#[arg(short, long, default_value = "10")]
|
||||
/// Number of evaluations to run concurrently (default: 3)
|
||||
#[arg(short, long, default_value = "3")]
|
||||
concurrency: usize,
|
||||
/// Maximum number of exercises to evaluate per language
|
||||
#[arg(long)]
|
||||
max_exercises_per_language: Option<usize>,
|
||||
}
|
||||
|
||||
// First, let's define the order in which templates should be executed
|
||||
const TEMPLATE_EXECUTION_ORDER: [&str; 3] = [
|
||||
"ProjectCreation",
|
||||
"CodeModification",
|
||||
"ConversationalGuidance",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let args = Args::parse();
|
||||
let http_client = Arc::new(ReqwestClient::new());
|
||||
let app = Application::headless().with_http_client(http_client.clone());
|
||||
|
||||
let crate_dir = PathBuf::from("../zed-agent-bench");
|
||||
let evaluation_data_dir = crate_dir.join("evaluation_data").canonicalize().unwrap();
|
||||
// Path to the zed-ace-framework repo
|
||||
let framework_path = PathBuf::from("../zed-ace-framework")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
|
||||
let repos_dir = crate_dir.join("repos");
|
||||
if !repos_dir.exists() {
|
||||
std::fs::create_dir_all(&repos_dir).unwrap();
|
||||
}
|
||||
let repos_dir = repos_dir.canonicalize().unwrap();
|
||||
// Fix the 'languages' lifetime issue by creating owned Strings instead of slices
|
||||
let languages: Vec<String> = args.languages.split(',').map(|s| s.to_string()).collect();
|
||||
|
||||
let all_evals = std::fs::read_dir(&evaluation_data_dir)
|
||||
.unwrap()
|
||||
.map(|path| path.unwrap().file_name().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let evals_to_run = if args.all {
|
||||
all_evals
|
||||
} else {
|
||||
args.eval_name_regexes
|
||||
.into_iter()
|
||||
.map(|regex_string| Regex::new(®ex_string).unwrap())
|
||||
.flat_map(|regex| {
|
||||
all_evals
|
||||
.iter()
|
||||
.filter(|eval_name| regex.is_match(eval_name))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
if evals_to_run.is_empty() {
|
||||
panic!("Names of evals to run must be provided or `--all` specified");
|
||||
}
|
||||
|
||||
println!("Will run the following evals: {evals_to_run:?}");
|
||||
println!("Running up to {} evals concurrently", args.concurrency);
|
||||
|
||||
let editor_model_name = if let Some(model_name) = args.editor_model_name {
|
||||
model_name
|
||||
} else {
|
||||
args.model_name.clone()
|
||||
};
|
||||
|
||||
let judge_model_name = if let Some(model_name) = args.judge_model_name {
|
||||
model_name
|
||||
} else {
|
||||
args.model_name.clone()
|
||||
};
|
||||
println!("Using zed-ace-framework at: {:?}", framework_path);
|
||||
println!("Evaluating languages: {:?}", languages);
|
||||
|
||||
app.run(move |cx| {
|
||||
let app_state = headless_assistant::init(cx);
|
||||
|
||||
let model = find_model(&args.model_name, cx).unwrap();
|
||||
let editor_model = find_model(&editor_model_name, cx).unwrap();
|
||||
let judge_model = find_model(&judge_model_name, cx).unwrap();
|
||||
let judge_model = if let Some(model_name) = &args.judge_model_name {
|
||||
find_model(model_name, cx).unwrap()
|
||||
} else {
|
||||
model.clone()
|
||||
};
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.set_active_model(Some(model.clone()), cx);
|
||||
registry.set_editor_model(Some(editor_model.clone()), cx);
|
||||
registry.set_default_model(Some(model.clone()), cx);
|
||||
});
|
||||
|
||||
let model_provider_id = model.provider_id();
|
||||
let editor_model_provider_id = editor_model.provider_id();
|
||||
let judge_model_provider_id = judge_model.provider_id();
|
||||
|
||||
let framework_path_clone = framework_path.clone();
|
||||
let languages_clone = languages.clone();
|
||||
let exercise_names = args.exercise_names.clone();
|
||||
let all_flag = args.all;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
// Authenticate all model providers first
|
||||
cx.update(|cx| authenticate_model_provider(model_provider_id.clone(), cx))
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| authenticate_model_provider(editor_model_provider_id.clone(), cx))
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| authenticate_model_provider(judge_model_provider_id.clone(), cx))
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let eval_load_futures = evals_to_run
|
||||
// Read base SHA from setup.json
|
||||
let base_sha = read_base_sha(&framework_path_clone).await.unwrap();
|
||||
|
||||
// Find all exercises for the specified languages
|
||||
let all_exercises = find_exercises(
|
||||
&framework_path_clone,
|
||||
&languages_clone
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
args.max_exercises_per_language,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Found {} exercises total", all_exercises.len());
|
||||
|
||||
// Filter exercises if specific ones were requested
|
||||
let exercises_to_run = if !exercise_names.is_empty() {
|
||||
// If exercise names are specified, filter by them regardless of --all flag
|
||||
all_exercises
|
||||
.into_iter()
|
||||
.filter(|path| {
|
||||
let name = get_exercise_name(path);
|
||||
exercise_names.iter().any(|filter| name.contains(filter))
|
||||
})
|
||||
.collect()
|
||||
} else if all_flag {
|
||||
// Only use all_flag if no exercise names are specified
|
||||
all_exercises
|
||||
} else {
|
||||
// Default behavior (no filters)
|
||||
all_exercises
|
||||
};
|
||||
|
||||
println!("Will run {} exercises", exercises_to_run.len());
|
||||
|
||||
// Get all templates and sort them according to the execution order
|
||||
let mut templates = all_templates();
|
||||
templates.sort_by_key(|template| {
|
||||
TEMPLATE_EXECUTION_ORDER
|
||||
.iter()
|
||||
.position(|&name| name == template.name)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
|
||||
// Create exercise eval tasks - each exercise is a single task that will run templates sequentially
|
||||
let exercise_tasks: Vec<_> = exercises_to_run
|
||||
.into_iter()
|
||||
.map(|eval_name| {
|
||||
let eval_path = evaluation_data_dir.join(&eval_name);
|
||||
let load_future = Eval::load(eval_name.clone(), eval_path, &repos_dir);
|
||||
.map(|exercise_path| {
|
||||
let exercise_name = get_exercise_name(&exercise_path);
|
||||
let templates_clone = templates.clone();
|
||||
let model_clone = model.clone();
|
||||
let judge_model_clone = judge_model.clone();
|
||||
let app_state_clone = app_state.clone();
|
||||
let base_sha_clone = base_sha.clone();
|
||||
let framework_path_clone = framework_path_clone.clone();
|
||||
let cx_clone = cx.clone();
|
||||
|
||||
async move {
|
||||
match load_future.await {
|
||||
Ok(eval) => Some(eval),
|
||||
println!("Processing exercise: {}", exercise_name);
|
||||
let mut exercise_results = Vec::new();
|
||||
|
||||
// Determine the language for this exercise
|
||||
let language = match get_exercise_language(&exercise_path) {
|
||||
Ok(lang) => lang,
|
||||
Err(err) => {
|
||||
// TODO: Persist errors / surface errors at the end.
|
||||
println!("Error loading {eval_name}: {err}");
|
||||
None
|
||||
println!(
|
||||
"Error determining language for {}: {}",
|
||||
exercise_name, err
|
||||
);
|
||||
return exercise_results;
|
||||
}
|
||||
};
|
||||
|
||||
// Run each template sequentially for this exercise
|
||||
for template in templates_clone {
|
||||
// For "multi" or "internal" language, only run the CodeModification template
|
||||
if (language == "multi" || language == "internal")
|
||||
&& template.name != "CodeModification"
|
||||
{
|
||||
println!(
|
||||
"Skipping {} template for {} language",
|
||||
template.name, language
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
match run_exercise_eval(
|
||||
exercise_path.clone(),
|
||||
template.clone(),
|
||||
model_clone.clone(),
|
||||
judge_model_clone.clone(),
|
||||
app_state_clone.clone(),
|
||||
base_sha_clone.clone(),
|
||||
framework_path_clone.clone(),
|
||||
cx_clone.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
println!(
|
||||
"Completed {} with template {} - score: {}",
|
||||
exercise_name, template.name, result.score
|
||||
);
|
||||
exercise_results.push(result);
|
||||
}
|
||||
Err(err) => {
|
||||
println!(
|
||||
"Error running {} with template {}: {}",
|
||||
exercise_name, template.name, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let loaded_evals = future::join_all(eval_load_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// The evals need to be loaded and grouped by URL before concurrently running, since
|
||||
// evals that use the same remote URL will use the same working directory.
|
||||
let mut evals_grouped_by_url: Vec<Vec<Eval>> = loaded_evals
|
||||
.into_iter()
|
||||
.map(|eval| (eval.eval_setup.url.clone(), eval))
|
||||
.into_group_map()
|
||||
.into_values()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort groups in descending order, so that bigger groups start first.
|
||||
evals_grouped_by_url.sort_by_key(|evals| cmp::Reverse(evals.len()));
|
||||
|
||||
let result_futures = evals_grouped_by_url
|
||||
.into_iter()
|
||||
.map(|evals| {
|
||||
let model = model.clone();
|
||||
let judge_model = judge_model.clone();
|
||||
let app_state = app_state.clone();
|
||||
let cx = cx.clone();
|
||||
|
||||
async move {
|
||||
let mut results = Vec::new();
|
||||
for eval in evals {
|
||||
let name = eval.name.clone();
|
||||
println!("Starting eval named {}", name);
|
||||
let result = run_eval(
|
||||
eval,
|
||||
model.clone(),
|
||||
judge_model.clone(),
|
||||
app_state.clone(),
|
||||
cx.clone(),
|
||||
)
|
||||
.await;
|
||||
results.push((name, result));
|
||||
// Save results for this exercise
|
||||
if !exercise_results.is_empty() {
|
||||
if let Err(err) =
|
||||
save_eval_results(&exercise_path, exercise_results.clone()).await
|
||||
{
|
||||
println!("Error saving results for {}: {}", exercise_name, err);
|
||||
} else {
|
||||
println!("Saved results for {}", exercise_name);
|
||||
}
|
||||
}
|
||||
results
|
||||
|
||||
exercise_results
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
let results = future::join_all(result_futures)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
println!(
|
||||
"Running {} exercises with concurrency: {}",
|
||||
exercise_tasks.len(),
|
||||
args.concurrency
|
||||
);
|
||||
|
||||
// Process results in order of completion
|
||||
for (eval_name, result) in results {
|
||||
match result {
|
||||
Ok((eval_output, judge_output)) => {
|
||||
println!("Generated diff for {eval_name}:\n");
|
||||
println!("{}\n", eval_output.diff);
|
||||
println!("Last message for {eval_name}:\n");
|
||||
println!("{}\n", eval_output.last_message);
|
||||
println!("Elapsed time: {:?}", eval_output.elapsed_time);
|
||||
println!(
|
||||
"Assistant response count: {}",
|
||||
eval_output.assistant_response_count
|
||||
);
|
||||
println!("Tool use counts: {:?}", eval_output.tool_use_counts);
|
||||
println!("Judge output for {eval_name}: {judge_output}");
|
||||
}
|
||||
Err(err) => {
|
||||
// TODO: Persist errors / surface errors at the end.
|
||||
println!("Error running {eval_name}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Run exercises concurrently, with each exercise running its templates sequentially
|
||||
let all_results = stream::iter(exercise_tasks)
|
||||
.buffer_unordered(args.concurrency)
|
||||
.flat_map(stream::iter)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
println!("Completed {} evaluation runs", all_results.len());
|
||||
cx.update(|cx| cx.quit()).unwrap();
|
||||
})
|
||||
.detach();
|
||||
@@ -226,18 +256,3 @@ fn main() {
|
||||
|
||||
println!("Done running evals");
|
||||
}
|
||||
|
||||
async fn run_eval(
|
||||
eval: Eval,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
judge_model: Arc<dyn LanguageModel>,
|
||||
app_state: Arc<HeadlessAppState>,
|
||||
cx: AsyncApp,
|
||||
) -> anyhow::Result<(EvalOutput, String)> {
|
||||
let path = eval.path.clone();
|
||||
let judge = Judge::load(&path, judge_model).await?;
|
||||
let eval_output = cx.update(|cx| eval.run(app_state, model, cx))?.await?;
|
||||
let judge_output = cx.update(|cx| judge.run(&eval_output, cx))?.await?;
|
||||
eval_output.save_to_directory(&path, judge_output.to_string())?;
|
||||
Ok((eval_output, judge_output))
|
||||
}
|
||||
|
||||
210
crates/assistant_eval/src/templates_eval.rs
Normal file
210
crates/assistant_eval/src/templates_eval.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Template {
|
||||
pub name: &'static str,
|
||||
pub content: &'static str,
|
||||
}
|
||||
|
||||
pub fn all_templates() -> Vec<Template> {
|
||||
vec![
|
||||
Template {
|
||||
name: "ProjectCreation",
|
||||
content: r#"
|
||||
# Project Creation Evaluation Template
|
||||
|
||||
## Instructions
|
||||
|
||||
Evaluate how well the AI assistant created a new implementation from scratch. Score it between 0.0 and 1.0 based on quality and fulfillment of requirements.
|
||||
- 1.0 = Perfect implementation that creates all necessary files with correct functionality.
|
||||
- 0.0 = Completely fails to create working files or meet requirements.
|
||||
|
||||
Note: A git diff output is required. If no code changes are provided (i.e., no git diff output), the score must be 0.0.
|
||||
|
||||
## Evaluation Criteria
|
||||
|
||||
Please consider the following aspects in order of importance:
|
||||
|
||||
1. **File Creation (25%)**
|
||||
- Did the assistant create all necessary files?
|
||||
- Are the files appropriately named and organized?
|
||||
- Did the assistant create a complete solution without missing components?
|
||||
|
||||
2. **Functional Correctness (40%)**
|
||||
- Does the implementation fulfill all specified requirements?
|
||||
- Does it handle edge cases properly?
|
||||
- Is it free of logical errors and bugs?
|
||||
- Do all components work together as expected?
|
||||
|
||||
3. **Code Quality (20%)**
|
||||
- Is the code well-structured, readable and well-documented?
|
||||
- Does it follow language-specific best practices?
|
||||
- Is there proper error handling?
|
||||
- Are naming conventions clear and consistent?
|
||||
|
||||
4. **Architecture Design (15%)**
|
||||
- Is the code modular and extensible?
|
||||
- Is there proper separation of concerns?
|
||||
- Are appropriate design patterns used?
|
||||
- Is the overall architecture appropriate for the requirements?
|
||||
|
||||
## Input
|
||||
|
||||
Requirements:
|
||||
<!-- ```requirements go here``` -->
|
||||
|
||||
Reference Implementation:
|
||||
<!-- ```reference code goes here``` -->
|
||||
|
||||
AI-Generated Implementation (git diff output):
|
||||
<!-- ```git diff goes here``` -->
|
||||
|
||||
## Output Format
|
||||
|
||||
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
|
||||
|
||||
EXAMPLE ONE:
|
||||
|
||||
0.92
|
||||
|
||||
EXAMPLE TWO:
|
||||
|
||||
0.85
|
||||
|
||||
EXAMPLE THREE:
|
||||
|
||||
0.78
|
||||
"#,
|
||||
},
|
||||
Template {
|
||||
name: "CodeModification",
|
||||
content: r#"
|
||||
# Code Modification Evaluation Template
|
||||
|
||||
## Instructions
|
||||
|
||||
Evaluate how well the AI assistant modified existing code to meet requirements. Score between 0.0 and 1.0 based on quality and appropriateness of changes.
|
||||
- 1.0 = Perfect modifications that correctly implement all requirements.
|
||||
- 0.0 = Failed to make appropriate changes or introduced serious errors.
|
||||
|
||||
## Evaluation Criteria
|
||||
|
||||
Please consider the following aspects in order of importance:
|
||||
|
||||
1. **Functional Correctness (50%)**
|
||||
- Do the modifications correctly implement the requirements?
|
||||
- Did the assistant modify the right files and code sections?
|
||||
- Are the changes free of bugs and logical errors?
|
||||
- Do the modifications maintain compatibility with existing code?
|
||||
|
||||
2. **Modification Approach (25%)**
|
||||
- Are the changes minimal and focused on what needs to be changed?
|
||||
- Did the assistant avoid unnecessary modifications?
|
||||
- Are the changes integrated seamlessly with the existing codebase?
|
||||
- Did the assistant preserve the original code style and patterns?
|
||||
|
||||
3. **Code Quality (15%)**
|
||||
- Are the modifications well-structured and documented?
|
||||
- Do they follow the same conventions as the original code?
|
||||
- Is there proper error handling in the modified code?
|
||||
- Are the changes readable and maintainable?
|
||||
|
||||
4. **Solution Completeness (10%)**
|
||||
- Do the modifications completely address all requirements?
|
||||
- Are there any missing changes or overlooked requirements?
|
||||
- Did the assistant consider all necessary edge cases?
|
||||
|
||||
## Input
|
||||
|
||||
Original:
|
||||
<!-- ```reference code goes here``` -->
|
||||
|
||||
New (git diff output):
|
||||
<!-- ```git diff goes here``` -->
|
||||
|
||||
## Output Format
|
||||
|
||||
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
|
||||
|
||||
EXAMPLE ONE:
|
||||
|
||||
0.92
|
||||
|
||||
EXAMPLE TWO:
|
||||
|
||||
0.85
|
||||
|
||||
EXAMPLE THREE:
|
||||
|
||||
0.78
|
||||
"#,
|
||||
},
|
||||
Template {
|
||||
name: "ConversationalGuidance",
|
||||
content: r#"
|
||||
# Conversational Guidance Evaluation Template
|
||||
|
||||
## Instructions
|
||||
|
||||
Evaluate the quality of the AI assistant's conversational guidance and score it between 0.0 and 1.0.
|
||||
- 1.0 = Perfect guidance with ideal information gathering, clarification, and advice without writing code.
|
||||
- 0.0 = Completely unhelpful, inappropriate guidance, or wrote code when it should not have.
|
||||
|
||||
## Evaluation Criteria
|
||||
|
||||
ABSOLUTE REQUIREMENT:
|
||||
- The assistant should NOT generate complete code solutions in conversation mode.
|
||||
- If the git diff shows the assistant wrote complete code, the score should be significantly reduced.
|
||||
|
||||
1. **Information Gathering Effectiveness (30%)**
|
||||
- Did the assistant ask relevant and precise questions?
|
||||
- Did it efficiently narrow down the problem scope?
|
||||
- Did it avoid unnecessary or redundant questions?
|
||||
- Was questioning appropriately paced and contextual?
|
||||
|
||||
2. **Conceptual Guidance (30%)**
|
||||
- Did the assistant provide high-level approaches and strategies?
|
||||
- Did it explain relevant concepts and algorithms?
|
||||
- Did it offer planning advice without implementing the solution?
|
||||
- Did it suggest a structured approach to solving the problem?
|
||||
|
||||
3. **Educational Value (20%)**
|
||||
- Did the assistant help the user understand the problem better?
|
||||
- Did it provide explanations that would help the user learn?
|
||||
- Did it guide without simply giving away answers?
|
||||
- Did it encourage the user to think through parts of the problem?
|
||||
|
||||
4. **Conversation Quality (20%)**
|
||||
- Was the conversation logically structured and easy to follow?
|
||||
- Did the assistant maintain appropriate context throughout?
|
||||
- Was the interaction helpful without being condescending?
|
||||
- Did the conversation reach a satisfactory conclusion with clear next steps?
|
||||
|
||||
## Input
|
||||
|
||||
Initial Query:
|
||||
<!-- ```query goes here``` -->
|
||||
|
||||
Conversation Transcript:
|
||||
<!-- ```transcript goes here``` -->
|
||||
|
||||
Git Diff:
|
||||
<!-- ```git diff goes here``` -->
|
||||
|
||||
## Output Format
|
||||
|
||||
THE ONLY OUTPUT SHOULD BE A SCORE BETWEEN 0.0 AND 1.0.
|
||||
|
||||
EXAMPLE ONE:
|
||||
|
||||
0.92
|
||||
|
||||
EXAMPLE TWO:
|
||||
|
||||
0.85
|
||||
|
||||
EXAMPLE THREE:
|
||||
|
||||
0.78
|
||||
"#,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -26,6 +26,7 @@ deepseek = { workspace = true, features = ["schemars"] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
||||
@@ -2,6 +2,29 @@ use std::sync::Arc;
|
||||
|
||||
use gpui::SharedString;
|
||||
use indexmap::IndexMap;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileId(pub Arc<str>);
|
||||
|
||||
impl AgentProfileId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AgentProfileId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AgentProfileId {
|
||||
fn default() -> Self {
|
||||
Self("write".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A profile for the Zed Agent that controls its behavior.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -9,6 +32,7 @@ pub struct AgentProfile {
|
||||
/// The name of the profile.
|
||||
pub name: SharedString,
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
pub enable_all_context_servers: bool,
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@ use std::sync::Arc;
|
||||
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use anyhow::{Result, bail};
|
||||
use deepseek::Model as DeepseekModel;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{App, Pixels};
|
||||
use indexmap::IndexMap;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use lmstudio::Model as LmStudioModel;
|
||||
use ollama::Model as OllamaModel;
|
||||
use schemars::{schema::Schema, JsonSchema};
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
@@ -26,6 +27,15 @@ pub enum AssistantDockPosition {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotifyWhenAgentWaiting {
|
||||
#[default]
|
||||
PrimaryScreen,
|
||||
AllScreens,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(tag = "name", rename_all = "snake_case")]
|
||||
pub enum AssistantProviderContentV1 {
|
||||
@@ -67,27 +77,45 @@ pub struct AssistantSettings {
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub editor_model: LanguageModelSelection,
|
||||
pub inline_assistant_model: Option<LanguageModelSelection>,
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub default_profile: Arc<str>,
|
||||
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
pub default_profile: AgentProfileId,
|
||||
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: bool,
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
|
||||
if cx.has_flag::<Assistant2FeatureFlag>() {
|
||||
return false;
|
||||
}
|
||||
|
||||
cx.is_staff() || self.enable_experimental_live_diffs
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.inline_assistant_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
self.commit_message_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
self.thread_summary_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
|
||||
/// Assistant panel settings
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AssistantSettingsContent {
|
||||
Versioned(VersionedAssistantSettingsContent),
|
||||
Versioned(Box<VersionedAssistantSettingsContent>),
|
||||
Legacy(LegacyAssistantSettingsContent),
|
||||
}
|
||||
|
||||
@@ -96,8 +124,8 @@ impl JsonSchema for AssistantSettingsContent {
|
||||
VersionedAssistantSettingsContent::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(gen)
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
VersionedAssistantSettingsContent::json_schema(r#gen)
|
||||
}
|
||||
|
||||
fn is_referenceable() -> bool {
|
||||
@@ -107,14 +135,14 @@ impl JsonSchema for AssistantSettingsContent {
|
||||
|
||||
impl Default for AssistantSettingsContent {
|
||||
fn default() -> Self {
|
||||
Self::Versioned(VersionedAssistantSettingsContent::default())
|
||||
Self::Versioned(Box::new(VersionedAssistantSettingsContent::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AssistantSettingsContent {
|
||||
pub fn is_version_outdated(&self) -> bool {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(_) => true,
|
||||
VersionedAssistantSettingsContent::V2(_) => false,
|
||||
},
|
||||
@@ -124,8 +152,8 @@ impl AssistantSettingsContent {
|
||||
|
||||
fn upgrade(&self) -> AssistantSettingsContentV2 {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref settings) => AssistantSettingsContentV2 {
|
||||
enabled: settings.enabled,
|
||||
button: settings.button,
|
||||
dock: settings.dock,
|
||||
@@ -172,7 +200,9 @@ impl AssistantSettingsContent {
|
||||
})
|
||||
}
|
||||
}),
|
||||
editor_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
@@ -180,7 +210,7 @@ impl AssistantSettingsContent {
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
|
||||
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
|
||||
enabled: None,
|
||||
@@ -197,7 +227,9 @@ impl AssistantSettingsContent {
|
||||
.id()
|
||||
.to_string(),
|
||||
}),
|
||||
editor_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
@@ -210,11 +242,11 @@ impl AssistantSettingsContent {
|
||||
|
||||
pub fn set_dock(&mut self, dock: AssistantDockPosition) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.dock = Some(dock);
|
||||
}
|
||||
},
|
||||
@@ -229,77 +261,79 @@ impl AssistantSettingsContent {
|
||||
let provider = language_model.provider_id().0.to_string();
|
||||
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
AssistantSettingsContent::Versioned(settings) => match **settings {
|
||||
VersionedAssistantSettingsContent::V1(ref mut settings) => {
|
||||
match provider.as_ref() {
|
||||
"zed.dev" => {
|
||||
log::warn!("attempted to set zed.dev model on outdated settings");
|
||||
}
|
||||
"anthropic" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Anthropic {
|
||||
default_model: AnthropicModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"ollama" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::Ollama {
|
||||
default_model: Some(ollama::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"lmstudio" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::LmStudio {
|
||||
default_model: Some(lmstudio::Model::new(&model, None, None)),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
"openai" => {
|
||||
let (api_url, available_models) = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::OpenAi {
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
..
|
||||
}) => (api_url.clone(), available_models.clone()),
|
||||
_ => (None, None),
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::OpenAi {
|
||||
default_model: OpenAiModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
available_models,
|
||||
});
|
||||
});
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
"deepseek" => {
|
||||
let api_url = match &settings.provider {
|
||||
Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
|
||||
api_url.clone()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
settings.provider = Some(AssistantProviderContentV1::DeepSeek {
|
||||
default_model: DeepseekModel::from_id(&model).ok(),
|
||||
api_url,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
@@ -311,17 +345,89 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: Arc<str>) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V1(_) => {}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => {}
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
if let AssistantSettingsContent::Versioned(boxed) = self {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.inline_assistant_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
if let AssistantSettingsContent::Versioned(boxed) = self {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.commit_message_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
if let AssistantSettingsContent::Versioned(boxed) = self {
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.thread_summary_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
|
||||
let AssistantSettingsContent::Versioned(boxed) = self else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.always_allow_tool_actions = Some(allow);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
|
||||
let AssistantSettingsContent::Versioned(boxed) = self else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_profile(
|
||||
&mut self,
|
||||
profile_id: AgentProfileId,
|
||||
profile: AgentProfile,
|
||||
) -> Result<()> {
|
||||
let AssistantSettingsContent::Versioned(boxed) = self else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
|
||||
let profiles = settings.profiles.get_or_insert_default();
|
||||
if profiles.contains_key(&profile_id) {
|
||||
bail!("profile with ID '{profile_id}' already exists");
|
||||
}
|
||||
|
||||
profiles.insert(
|
||||
profile_id,
|
||||
AgentProfileContent {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
enable_all_context_servers: Some(profile.enable_all_context_servers),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
.map(|(server_id, preset)| {
|
||||
(
|
||||
server_id,
|
||||
ContextServerPresetContent {
|
||||
tools: preset.tools,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -342,7 +448,9 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
default_model: None,
|
||||
editor_model: None,
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
@@ -375,29 +483,35 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: 320
|
||||
default_height: Option<f32>,
|
||||
/// The default model to use when creating new chats.
|
||||
/// The default model to use when creating new chats and for other features when a specific model is not specified.
|
||||
default_model: Option<LanguageModelSelection>,
|
||||
/// The model to use when applying edits from the assistant.
|
||||
editor_model: Option<LanguageModelSelection>,
|
||||
/// Model to use for the inline assistant. Defaults to default_model when not specified.
|
||||
inline_assistant_model: Option<LanguageModelSelection>,
|
||||
/// Model to use for generating git commit messages. Defaults to default_model when not specified.
|
||||
commit_message_model: Option<LanguageModelSelection>,
|
||||
/// Model to use for generating thread summaries. Defaults to default_model when not specified.
|
||||
thread_summary_model: Option<LanguageModelSelection>,
|
||||
/// Additional models with which to generate alternatives when performing inline assists.
|
||||
inline_alternatives: Option<Vec<LanguageModelSelection>>,
|
||||
/// Enable experimental live diffs in the assistant panel.
|
||||
///
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
#[schemars(skip)]
|
||||
default_profile: Option<Arc<str>>,
|
||||
#[schemars(skip)]
|
||||
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
|
||||
/// The default profile to use in the Agent.
|
||||
///
|
||||
/// Default: write
|
||||
default_profile: Option<AgentProfileId>,
|
||||
/// The available agent profiles.
|
||||
pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
|
||||
/// Whenever a tool action would normally wait for your confirmation
|
||||
/// that you allow it, always choose to allow it.
|
||||
///
|
||||
/// Default: false
|
||||
always_allow_tool_actions: Option<bool>,
|
||||
/// Whether to show a popup notification when the agent is waiting for user input.
|
||||
/// Where to show a popup notification when the agent is waiting for user input.
|
||||
///
|
||||
/// Default: true
|
||||
notify_when_agent_waiting: Option<bool>,
|
||||
/// Default: "primary_screen"
|
||||
notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
@@ -407,7 +521,7 @@ pub struct LanguageModelSelection {
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
@@ -437,7 +551,10 @@ impl Default for LanguageModelSelection {
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileContent {
|
||||
pub name: Arc<str>,
|
||||
#[serde(default)]
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
/// Whether all context servers are enabled by default.
|
||||
pub enable_all_context_servers: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
|
||||
}
|
||||
@@ -535,7 +652,15 @@ impl Settings for AssistantSettings {
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model);
|
||||
merge(&mut settings.editor_model, value.editor_model);
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.or(settings.inline_assistant_model.take());
|
||||
settings.commit_message_model = value
|
||||
.commit_message_model
|
||||
.or(settings.commit_message_model.take());
|
||||
settings.thread_summary_model = value
|
||||
.thread_summary_model
|
||||
.or(settings.thread_summary_model.take());
|
||||
merge(&mut settings.inline_alternatives, value.inline_alternatives);
|
||||
merge(
|
||||
&mut settings.enable_experimental_live_diffs,
|
||||
@@ -560,6 +685,9 @@ impl Settings for AssistantSettings {
|
||||
AgentProfile {
|
||||
name: profile.name.into(),
|
||||
tools: profile.tools,
|
||||
enable_all_context_servers: profile
|
||||
.enable_all_context_servers
|
||||
.unwrap_or_default(),
|
||||
context_servers: profile
|
||||
.context_servers
|
||||
.into_iter()
|
||||
@@ -623,16 +751,15 @@ mod tests {
|
||||
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
|
||||
fs.clone(),
|
||||
|settings, _| {
|
||||
*settings = AssistantSettingsContent::Versioned(
|
||||
*settings = AssistantSettingsContent::Versioned(Box::new(
|
||||
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
editor_model: Some(LanguageModelSelection {
|
||||
provider: "test-provider".into(),
|
||||
model: "gpt-99".into(),
|
||||
}),
|
||||
inline_assistant_model: None,
|
||||
commit_message_model: None,
|
||||
thread_summary_model: None,
|
||||
inline_alternatives: None,
|
||||
enabled: None,
|
||||
button: None,
|
||||
@@ -645,7 +772,7 @@ mod tests {
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
}),
|
||||
)
|
||||
))
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -6,17 +6,17 @@ pub use crate::extension_slash_command::*;
|
||||
pub use crate::slash_command_registry::*;
|
||||
pub use crate::slash_command_working_set::*;
|
||||
use anyhow::Result;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
ops::Range,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use workspace::{ui::IconName, Workspace};
|
||||
use workspace::{Workspace, ui::IconName};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
SlashCommandRegistry::default_global(cx);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
@@ -29,12 +29,9 @@ html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
rope.workspace = true
|
||||
schemars.workspace = true
|
||||
semantic_index.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
@@ -45,6 +42,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user