Compare commits
392 Commits
fix-git-wo
...
zeta2-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4ee4fed7 | ||
|
|
ea4bf46a36 | ||
|
|
05545abab6 | ||
|
|
a85608566d | ||
|
|
69af5261ea | ||
|
|
b9e2f61a38 | ||
|
|
38bbb497dd | ||
|
|
0cc7b4a93c | ||
|
|
cc32bfdfdf | ||
|
|
50de8ddc28 | ||
|
|
f770011d7f | ||
|
|
f2a6b57909 | ||
|
|
96b67ac70e | ||
|
|
64d362cbce | ||
|
|
5d561aa494 | ||
|
|
4ee2daeded | ||
|
|
c27d8e0c7a | ||
|
|
f6c5c68751 | ||
|
|
74e5b848ff | ||
|
|
ee399ebccf | ||
|
|
54c82f2732 | ||
|
|
e14a4ab90d | ||
|
|
0343b5ff06 | ||
|
|
26202e5af2 | ||
|
|
ee912366a3 | ||
|
|
673a98a277 | ||
|
|
5674445a61 | ||
|
|
53513cab23 | ||
|
|
e885a939ba | ||
|
|
a01a2ed0e0 | ||
|
|
af3bc45a26 | ||
|
|
173074f248 | ||
|
|
a7cb64c64d | ||
|
|
c6472fd7a8 | ||
|
|
c0710fa8ca | ||
|
|
f321d02207 | ||
|
|
1c09985fb3 | ||
|
|
d986077592 | ||
|
|
555b6ee4e5 | ||
|
|
6446963a0c | ||
|
|
ceb907e0dc | ||
|
|
3dbccc828e | ||
|
|
853e625259 | ||
|
|
0784bb8192 | ||
|
|
9046091164 | ||
|
|
6384966ab5 | ||
|
|
8b9c74726a | ||
|
|
63586ff2e4 | ||
|
|
35e5aa4e71 | ||
|
|
7ea94a32be | ||
|
|
6d6c3d648a | ||
|
|
53b2f37452 | ||
|
|
92b946e8e5 | ||
|
|
e9b4f59e0f | ||
|
|
989adde57b | ||
|
|
393d6787a3 | ||
|
|
4a582504d4 | ||
|
|
cfb2925169 | ||
|
|
14f4e867aa | ||
|
|
4d54ccf494 | ||
|
|
5b1c87b6a6 | ||
|
|
0fef17baa2 | ||
|
|
526196917b | ||
|
|
a598fbaa73 | ||
|
|
634ae72cad | ||
|
|
98edf1bf0b | ||
|
|
1090c47a90 | ||
|
|
be7b22b0dc | ||
|
|
f3e49e1b05 | ||
|
|
0adc6ddaad | ||
|
|
99b71677c6 | ||
|
|
1c27a6dbc2 | ||
|
|
256a91019a | ||
|
|
85aa458b9c | ||
|
|
37239fd66b | ||
|
|
2b1f7d5763 | ||
|
|
813a9bb0bc | ||
|
|
e40a950bc4 | ||
|
|
89e527c23b | ||
|
|
c50b561e1c | ||
|
|
13113ab311 | ||
|
|
01f181339f | ||
|
|
d046016ef5 | ||
|
|
e43ad858d8 | ||
|
|
ded6467604 | ||
|
|
53c5db4495 | ||
|
|
cd2ecbbd27 | ||
|
|
e71012a2f8 | ||
|
|
b9cf5886e4 | ||
|
|
174a0b1517 | ||
|
|
e4b754a19f | ||
|
|
5f20b905a5 | ||
|
|
4c758bd0b7 | ||
|
|
4b7595c94c | ||
|
|
2143c59fba | ||
|
|
2b3ca360c3 | ||
|
|
85f7bb6277 | ||
|
|
7377a898e8 | ||
|
|
8ebe812c24 | ||
|
|
7f1c7c1910 | ||
|
|
503284db45 | ||
|
|
2aa564eeb7 | ||
|
|
cba9ff55c7 | ||
|
|
a577128163 | ||
|
|
687c2c88c7 | ||
|
|
2a03b6b80c | ||
|
|
e68aa18fd4 | ||
|
|
592b013013 | ||
|
|
1142408675 | ||
|
|
8201f3d72f | ||
|
|
fcfc54c515 | ||
|
|
ffb85d7e81 | ||
|
|
405d7d7476 | ||
|
|
bdf44e55aa | ||
|
|
45ee1327a4 | ||
|
|
b60e705782 | ||
|
|
2bb50acb58 | ||
|
|
87f5e72fc0 | ||
|
|
11b7913956 | ||
|
|
ff2eebf522 | ||
|
|
c4d75ea6d5 | ||
|
|
d5d30b5c44 | ||
|
|
7655e22ff5 | ||
|
|
7a83a7fbd0 | ||
|
|
3cb3f01406 | ||
|
|
46aa05e240 | ||
|
|
a33af4e9c0 | ||
|
|
116c6549f6 | ||
|
|
da8c7a1256 | ||
|
|
2b04186b0f | ||
|
|
462293667b | ||
|
|
e5c0373011 | ||
|
|
a066794e8d | ||
|
|
f6b6d4a9fe | ||
|
|
238dab4a9c | ||
|
|
d1c6c9d035 | ||
|
|
d7f3d08c59 | ||
|
|
4db19a3a96 | ||
|
|
c4e8fe1fb7 | ||
|
|
4002602a89 | ||
|
|
6ae83b4740 | ||
|
|
eec6bfebbb | ||
|
|
9875969cba | ||
|
|
59502289e7 | ||
|
|
f764077020 | ||
|
|
9708c8d507 | ||
|
|
f205732074 | ||
|
|
aee21ca17f | ||
|
|
816c4817d0 | ||
|
|
0f9232a10d | ||
|
|
db367cc6bf | ||
|
|
2ce0641fe0 | ||
|
|
95ccce3095 | ||
|
|
14de161d06 | ||
|
|
b8c30f448f | ||
|
|
cb75c2aeb7 | ||
|
|
2c29eac29f | ||
|
|
a94b0931c7 | ||
|
|
441a934d84 | ||
|
|
b28c979aae | ||
|
|
22e31a0d41 | ||
|
|
c0b583c9ef | ||
|
|
6441099a67 | ||
|
|
611b96627b | ||
|
|
630340d659 | ||
|
|
acb3406eb8 | ||
|
|
fb3c991112 | ||
|
|
d110d325d4 | ||
|
|
2cf3def716 | ||
|
|
df2d097dc5 | ||
|
|
fcdd427cf8 | ||
|
|
9c548a0ec6 | ||
|
|
bd0a5dd664 | ||
|
|
2f40a3bdfa | ||
|
|
304af661a0 | ||
|
|
d2886d606b | ||
|
|
cffb883108 | ||
|
|
eb7154d099 | ||
|
|
18c6d9d394 | ||
|
|
414d3be437 | ||
|
|
0862a0b666 | ||
|
|
2e36e9782e | ||
|
|
1751bf4cdb | ||
|
|
2fae4c7c72 | ||
|
|
1ae3d25aed | ||
|
|
5e58f44d85 | ||
|
|
d8085d3ac0 | ||
|
|
707d0e6ebd | ||
|
|
46fb521333 | ||
|
|
9529cd18d1 | ||
|
|
14ffd7b53f | ||
|
|
9431c65733 | ||
|
|
b2d7e34e80 | ||
|
|
61d4718f2b | ||
|
|
9e903c9fd1 | ||
|
|
d81479ee57 | ||
|
|
a2edd56587 | ||
|
|
d7a9be03d1 | ||
|
|
cdbddc2170 | ||
|
|
f397294640 | ||
|
|
8527dcfc65 | ||
|
|
7f607a9b7d | ||
|
|
5e397e85b1 | ||
|
|
ad02f6b9e3 | ||
|
|
2e7607c0e7 | ||
|
|
0ac1752668 | ||
|
|
af1875f91c | ||
|
|
734f94b71c | ||
|
|
136468a4df | ||
|
|
adf43d691a | ||
|
|
466a2e22d5 | ||
|
|
365c5ab45f | ||
|
|
11d81b95d4 | ||
|
|
4b3b2acf75 | ||
|
|
849424740f | ||
|
|
3e605c2c4b | ||
|
|
82b11bf77c | ||
|
|
3a437fd888 | ||
|
|
96c429d2c3 | ||
|
|
ea4073e50e | ||
|
|
8c93112869 | ||
|
|
1feffad5e8 | ||
|
|
ae54a4e1b8 | ||
|
|
4a0a7d1d27 | ||
|
|
5934d3789b | ||
|
|
acde79dae7 | ||
|
|
246c644316 | ||
|
|
e4de26e5dc | ||
|
|
7091c70a1e | ||
|
|
fa0df6da1c | ||
|
|
99102a84fa | ||
|
|
5f01f6d75f | ||
|
|
a66cd820b3 | ||
|
|
f07da9d9f2 | ||
|
|
8d05bb090c | ||
|
|
2325f14713 | ||
|
|
fe2aa3f4cb | ||
|
|
10989c702c | ||
|
|
3f80ac0127 | ||
|
|
4f1634f95c | ||
|
|
40eec32cb8 | ||
|
|
17499453f6 | ||
|
|
80a4746a46 | ||
|
|
01f5b73e3b | ||
|
|
a0081dd693 | ||
|
|
f522823988 | ||
|
|
5a8603bebb | ||
|
|
abac87c2f8 | ||
|
|
c3d065cecc | ||
|
|
e1a5d29972 | ||
|
|
d342da4e9a | ||
|
|
7ae8f81d74 | ||
|
|
36364b16a0 | ||
|
|
b35959f4c2 | ||
|
|
9450bcad25 | ||
|
|
69bdef38ec | ||
|
|
0e33a3afe0 | ||
|
|
76aaf6a8fe | ||
|
|
0ef7ee172f | ||
|
|
29def012a1 | ||
|
|
5c30578c49 | ||
|
|
1552afd8bf | ||
|
|
e04473dd26 | ||
|
|
84f166fc85 | ||
|
|
065518577e | ||
|
|
1d828b6ac6 | ||
|
|
777ce7cc97 | ||
|
|
1f37fbd051 | ||
|
|
8c9442ad11 | ||
|
|
47a475681f | ||
|
|
23dc1f5ea4 | ||
|
|
a6a111cadd | ||
|
|
6a7b84eb87 | ||
|
|
59bdbf5a5d | ||
|
|
64b6e8ba0f | ||
|
|
236b3e546e | ||
|
|
ea363466aa | ||
|
|
c45177e296 | ||
|
|
45fa034107 | ||
|
|
1c5c8552f2 | ||
|
|
5d374193bb | ||
|
|
b65fb06264 | ||
|
|
b3405c3bd1 | ||
|
|
638320b21e | ||
|
|
91ab0636ec | ||
|
|
fb6cc8794f | ||
|
|
3d37611b6f | ||
|
|
360e372b57 | ||
|
|
74e8afe9a8 | ||
|
|
e30f45cf64 | ||
|
|
16c4fd4fc5 | ||
|
|
ec58adca13 | ||
|
|
bed358718b | ||
|
|
4124bedab7 | ||
|
|
57c6dbd71e | ||
|
|
fded3fbcdb | ||
|
|
a660527036 | ||
|
|
0cb8a8983c | ||
|
|
c7902478c1 | ||
|
|
3c0183fa5e | ||
|
|
e982cb824a | ||
|
|
1b865a60f8 | ||
|
|
4c32d5bf13 | ||
|
|
ccae033d85 | ||
|
|
c2fa9d7981 | ||
|
|
5f03202b5c | ||
|
|
223fda2fe2 | ||
|
|
a85946eba8 | ||
|
|
9d94358971 | ||
|
|
9e11105483 | ||
|
|
caebd0cc4d | ||
|
|
6e2922367c | ||
|
|
25ee9b1013 | ||
|
|
0870a1fe80 | ||
|
|
e37efc1e9b | ||
|
|
1ae326432e | ||
|
|
a05f86f97b | ||
|
|
473bbd78cc | ||
|
|
28c78d2d85 | ||
|
|
fca44f89c1 | ||
|
|
b7ad20773c | ||
|
|
aa1629b544 | ||
|
|
69a5c45672 | ||
|
|
d0aaf04673 | ||
|
|
d677c98f43 | ||
|
|
ce362864db | ||
|
|
3c021d0890 | ||
|
|
f36a545a86 | ||
|
|
9eeeda1330 | ||
|
|
da2d791127 | ||
|
|
d6f0811dab | ||
|
|
be0bb4a56b | ||
|
|
bf1ae1d196 | ||
|
|
3b7dbb87b0 | ||
|
|
bb13228ad5 | ||
|
|
ec1528b890 | ||
|
|
2aa0114b40 | ||
|
|
bb2d833373 | ||
|
|
eedfc5be5a | ||
|
|
0e76cc8036 | ||
|
|
6bd5251882 | ||
|
|
13de400a2a | ||
|
|
c3480c3d6f | ||
|
|
0cbacb8500 | ||
|
|
7327ef662b | ||
|
|
c1ca7303a8 | ||
|
|
92283285ae | ||
|
|
d80f9dda75 | ||
|
|
ebc22c290b | ||
|
|
7633bbf55a | ||
|
|
91cbb2ec25 | ||
|
|
40199266b6 | ||
|
|
9a8c5053c2 | ||
|
|
c446662862 | ||
|
|
6feae92616 | ||
|
|
ae840c6ef3 | ||
|
|
d7fd5910d7 | ||
|
|
8d5861322b | ||
|
|
5a9e18603d | ||
|
|
2a7761fe17 | ||
|
|
f23096034b | ||
|
|
1ed17fdd94 | ||
|
|
7ea7f4e767 | ||
|
|
035d7ddcf8 | ||
|
|
9d67276090 | ||
|
|
161d128d45 | ||
|
|
e1b0a98c34 | ||
|
|
ae0ee70abd | ||
|
|
893eb92f91 | ||
|
|
45fa6d81ac | ||
|
|
60ad82cc94 | ||
|
|
564ded71c1 | ||
|
|
63b3839a83 | ||
|
|
9f749881b3 | ||
|
|
946efb03df | ||
|
|
4b96ad3fba | ||
|
|
4368c1b56b | ||
|
|
e5a968b709 | ||
|
|
7aecab8e14 | ||
|
|
e4df866664 | ||
|
|
8770fcc841 | ||
|
|
6dcae2711d | ||
|
|
5e01fb8f1c | ||
|
|
88a79750cc | ||
|
|
4c411b9fc8 | ||
|
|
5ac6ae501f | ||
|
|
c01f12b15d | ||
|
|
dfa066dfe8 | ||
|
|
ac8c653ae6 | ||
|
|
d2318be8d9 | ||
|
|
a026163746 | ||
|
|
ad3ddd381d |
@@ -19,8 +19,6 @@ rustflags = [
|
||||
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
"-C",
|
||||
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
|
||||
"-C",
|
||||
"link-arg=-fuse-ld=lld",
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
@@ -26,7 +26,7 @@ third-party = [
|
||||
# build of remote_server should not include scap / its x11 dependency
|
||||
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
|
||||
# build of remote_server should not need to include on libalsa through rodio
|
||||
{ name = "rodio" },
|
||||
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
|
||||
]
|
||||
|
||||
[final-excludes]
|
||||
@@ -41,5 +41,4 @@ workspace-members = [
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
"zed_toml",
|
||||
]
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -2,4 +2,4 @@
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
|
||||
# Ensure the WSL script always has LF line endings, even on Windows
|
||||
crates/zed/resources/windows/zed-wsl text eol=lf
|
||||
crates/zed/resources/windows/zed.sh text eol=lf
|
||||
|
||||
159
.github/actions/run_tests_windows/action.yml
vendored
159
.github/actions/run_tests_windows/action.yml
vendored
@@ -20,167 +20,8 @@ runs:
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Configure crash dumps
|
||||
shell: powershell
|
||||
run: |
|
||||
# Record the start time for this CI run
|
||||
$runStartTime = Get-Date
|
||||
$runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
Write-Host "CI run started at: $runStartTimeStr"
|
||||
|
||||
# Save the timestamp for later use
|
||||
echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
|
||||
|
||||
# Create crash dump directory in workspace (non-persistent)
|
||||
$dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
|
||||
New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
|
||||
|
||||
Write-Host "Setting up crash dump detection..."
|
||||
Write-Host "Workspace dump path: $dumpPath"
|
||||
|
||||
# Note: We're NOT modifying registry on stateful runners
|
||||
# Instead, we'll check default Windows crash locations after tests
|
||||
|
||||
- name: Run tests
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
$env:RUST_BACKTRACE = "full"
|
||||
|
||||
# Enable Windows debugging features
|
||||
$env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
|
||||
|
||||
# .NET crash dump environment variables (ephemeral)
|
||||
$env:COMPlus_DbgEnableMiniDump = "1"
|
||||
$env:COMPlus_DbgMiniDumpType = "4"
|
||||
$env:COMPlus_CreateDumpDiagnostics = "1"
|
||||
|
||||
cargo nextest run --workspace --no-fail-fast
|
||||
|
||||
- name: Analyze crash dumps
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
Write-Host "Checking for crash dumps..."
|
||||
|
||||
# Get the CI run start time from the environment
|
||||
$runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
|
||||
Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
|
||||
|
||||
# Check all possible crash dump locations
|
||||
$searchPaths = @(
|
||||
"$env:GITHUB_WORKSPACE\crash_dumps",
|
||||
"$env:LOCALAPPDATA\CrashDumps",
|
||||
"$env:TEMP",
|
||||
"$env:GITHUB_WORKSPACE",
|
||||
"$env:USERPROFILE\AppData\Local\CrashDumps",
|
||||
"C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
|
||||
)
|
||||
|
||||
$dumps = @()
|
||||
foreach ($path in $searchPaths) {
|
||||
if (Test-Path $path) {
|
||||
Write-Host "Searching in: $path"
|
||||
$found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.CreationTime -gt $runStartTime
|
||||
}
|
||||
if ($found) {
|
||||
$dumps += $found
|
||||
Write-Host " Found $($found.Count) dump(s) from this CI run"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($dumps) {
|
||||
Write-Host "Found $($dumps.Count) crash dump(s)"
|
||||
|
||||
# Install debugging tools if not present
|
||||
$cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
|
||||
if (-not (Test-Path $cdbPath)) {
|
||||
Write-Host "Installing Windows Debugging Tools..."
|
||||
$url = "https://go.microsoft.com/fwlink/?linkid=2237387"
|
||||
Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
|
||||
Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
|
||||
}
|
||||
|
||||
foreach ($dump in $dumps) {
|
||||
Write-Host "`n=================================="
|
||||
Write-Host "Analyzing crash dump: $($dump.Name)"
|
||||
Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
|
||||
Write-Host "Time: $($dump.CreationTime)"
|
||||
Write-Host "=================================="
|
||||
|
||||
# Set symbol path
|
||||
$env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
|
||||
|
||||
# Run analysis
|
||||
$analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
|
||||
|
||||
# Extract key information
|
||||
if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
|
||||
Write-Host "Exception Code: $($Matches[1])"
|
||||
if ($Matches[1] -eq "c0000005") {
|
||||
Write-Host "Exception Type: ACCESS VIOLATION"
|
||||
}
|
||||
}
|
||||
|
||||
if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
|
||||
Write-Host "Exception Record: $($Matches[1])"
|
||||
}
|
||||
|
||||
if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
|
||||
Write-Host "Faulting Instruction: $($Matches[1])"
|
||||
}
|
||||
|
||||
# Save full analysis
|
||||
$analysisFile = "$($dump.FullName).analysis.txt"
|
||||
$analysisOutput | Out-File -FilePath $analysisFile
|
||||
Write-Host "`nFull analysis saved to: $analysisFile"
|
||||
|
||||
# Print stack trace section
|
||||
Write-Host "`n--- Stack Trace Preview ---"
|
||||
$stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
|
||||
$stackLines = $stackSection -split "`n" | Select-Object -First 20
|
||||
$stackLines | ForEach-Object { Write-Host $_ }
|
||||
Write-Host "--- End Stack Trace Preview ---"
|
||||
}
|
||||
|
||||
Write-Host "`n⚠️ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
|
||||
|
||||
# Copy dumps to workspace for artifact upload
|
||||
$artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
|
||||
New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
|
||||
|
||||
foreach ($dump in $dumps) {
|
||||
$destName = "$($dump.Directory.Name)_$($dump.Name)"
|
||||
Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
|
||||
if (Test-Path "$($dump.FullName).analysis.txt") {
|
||||
Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
|
||||
} else {
|
||||
Write-Host "No crash dumps from this CI run found"
|
||||
}
|
||||
|
||||
- name: Upload crash dumps
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
path: |
|
||||
crash_dumps_collected/*.dmp
|
||||
crash_dumps_collected/*.txt
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
- name: Check test results
|
||||
shell: powershell
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: |
|
||||
# Re-check test results to fail the job if tests failed
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Tests failed with exit code: $LASTEXITCODE"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
41
.github/workflows/ci.yml
vendored
41
.github/workflows/ci.yml
vendored
@@ -81,6 +81,7 @@ jobs:
|
||||
echo "run_license=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
|
||||
echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
|
||||
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_nix=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -372,6 +373,46 @@ jobs:
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
doctests:
|
||||
# Nextest currently doesn't support doctests, so run them separately and in parallel.
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Run doctests
|
||||
needs: [job_spec]
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on:
|
||||
- namespace-profile-16x32-ubuntu-2204
|
||||
steps:
|
||||
- name: Add Rust to the PATH
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
clean: false
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
# cache-provider: "buildjet"
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
|
||||
- name: Configure CI
|
||||
run: |
|
||||
mkdir -p ./../.cargo
|
||||
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
|
||||
|
||||
- name: Run doctests
|
||||
run: cargo test --workspace --doc --no-fail-fast
|
||||
|
||||
- name: Clean CI config file
|
||||
if: always()
|
||||
run: rm -rf ./../.cargo
|
||||
|
||||
build_remote_server:
|
||||
timeout-minutes: 60
|
||||
name: (Linux) Build Remote Server
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
send_release_notes_email:
|
||||
|
||||
57
.github/workflows/congrats.yml
vendored
Normal file
57
.github/workflows/congrats.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Congratsbot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check-author:
|
||||
if: ${{ github.repository_owner == 'zed-industries' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_congratulate: ${{ steps.check.outputs.should_congratulate }}
|
||||
steps:
|
||||
- name: Get PR info and check if author is external
|
||||
id: check
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.CONGRATSBOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
commit_sha: context.sha
|
||||
});
|
||||
|
||||
if (prs.length === 0) {
|
||||
core.setOutput('should_congratulate', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const mergedPR = prs.find(pr => pr.merged_at !== null) || prs[0];
|
||||
const prAuthor = mergedPR.user.login;
|
||||
|
||||
try {
|
||||
await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: 'zed-industries',
|
||||
team_slug: 'staff',
|
||||
username: prAuthor
|
||||
});
|
||||
core.setOutput('should_congratulate', 'false');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.setOutput('should_congratulate', 'true');
|
||||
} else {
|
||||
console.error(`Error checking team membership: ${error.message}`);
|
||||
core.setOutput('should_congratulate', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
congrats:
|
||||
needs: check-author
|
||||
if: needs.check-author.outputs.should_congratulate == 'true'
|
||||
uses: withastro/automation/.github/workflows/congratsbot.yml@main
|
||||
with:
|
||||
EMOJIS: 🎉,🎊,🧑🚀,🥳,🙌,🚀,🦀,🔥,🚢
|
||||
secrets:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_CONGRATS }}
|
||||
36
.github/workflows/good_first_issue_notifier.yml
vendored
Normal file
36
.github/workflows/good_first_issue_notifier.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Good First Issue Notifier
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
handle-good-first-issue:
|
||||
if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Prepare Discord message
|
||||
id: prepare-message
|
||||
env:
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
run: |
|
||||
MESSAGE="[${ISSUE_TITLE} (#${ISSUE_NUMBER})](<${ISSUE_URL}>)"
|
||||
|
||||
{
|
||||
echo "message<<EOF"
|
||||
echo "$MESSAGE"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_GOOD_FIRST_ISSUE }}
|
||||
content: ${{ steps.prepare-message.outputs.message }}
|
||||
15
.rules
15
.rules
@@ -12,6 +12,19 @@
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
|
||||
* Avoid creative additions unless explicitly requested
|
||||
* Use full words for variable names (no abbreviations like "q" for "queue")
|
||||
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
|
||||
Example:
|
||||
```rust
|
||||
executor.spawn({
|
||||
let task_ran = task_ran.clone();
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
# GPUI
|
||||
|
||||
@@ -46,7 +59,7 @@ Trying to update an entity while it's already being updated must be avoided as t
|
||||
|
||||
When `read_with`, `update`, or `update_in` are used with an async context, the closure's return value is wrapped in an `anyhow::Result`.
|
||||
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to eachother they will never be dropped.
|
||||
`WeakEntity<T>` is a weak handle. It has `read_with`, `update`, and `update_in` methods that work the same, but always return an `anyhow::Result` so that they can fail if the entity no longer exists. This can be useful to avoid memory leaks - if entities have mutually recursive handles to each other they will never be dropped.
|
||||
|
||||
## Concurrency
|
||||
|
||||
|
||||
@@ -1,71 +1,74 @@
|
||||
# Contributing to Zed
|
||||
|
||||
Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor!
|
||||
Thank you for helping us make Zed better!
|
||||
|
||||
All activity in Zed forums is subject to our [Code of Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign our [Contributor License Agreement](https://zed.dev/cla) before their contributions can be merged.
|
||||
All activity in Zed forums is subject to our [Code of
|
||||
Conduct](https://zed.dev/code-of-conduct). Additionally, contributors must sign
|
||||
our [Contributor License Agreement](https://zed.dev/cla) before their
|
||||
contributions can be merged.
|
||||
|
||||
## Contribution ideas
|
||||
|
||||
If you're looking for ideas about what to work on, check out:
|
||||
Zed is a large project with a number of priorities. We spend most of
|
||||
our time working on what we believe the product needs, but we also love working
|
||||
with the community to improve the product in ways we haven't thought of (or had time to get to yet!)
|
||||
|
||||
In particular we love PRs that are:
|
||||
|
||||
- Fixes to existing bugs and issues.
|
||||
- Small enhancements to existing features, particularly to make them work for more people.
|
||||
- Small extra features, like keybindings or actions you miss from other editors or extensions.
|
||||
- Work towards shipping larger features on our roadmap.
|
||||
|
||||
If you're looking for concrete ideas:
|
||||
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
- Our [top-ranking issues](https://github.com/zed-industries/zed/issues/5393) based on votes by the community.
|
||||
- Our [public roadmap](https://zed.dev/roadmap) contains a rough outline of our near-term priorities for Zed.
|
||||
|
||||
For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
||||
## Sending changes
|
||||
|
||||
## Proposing changes
|
||||
The Zed culture values working code and synchronous conversations over long
|
||||
discussion threads.
|
||||
|
||||
The best way to propose a change is to [start a discussion on our GitHub repository](https://github.com/zed-industries/zed/discussions).
|
||||
The best way to get us to take a look at a proposed change is to send a pull
|
||||
request. We will get back to you (though this sometimes takes longer than we'd
|
||||
like, sorry).
|
||||
|
||||
First, write a short **problem statement**, which _clearly_ and _briefly_ describes the problem you want to solve independently from any specific solution. It doesn't need to be long or formal, but it's difficult to consider a solution in absence of a clear understanding of the problem.
|
||||
Although we will take a look, we tend to only merge about half the PRs that are
|
||||
submitted. If you'd like your PR to have the best chance of being merged:
|
||||
|
||||
Next, write a short **solution proposal**. How can the problem (or set of problems) you have stated above be addressed? What are the pros and cons of your approach? Again, keep it brief and informal. This isn't a specification, but rather a starting point for a conversation.
|
||||
- Include a clear description of what you're solving, and why it's important to you.
|
||||
- Include tests.
|
||||
- If it changes the UI, attach screenshots or screen recordings.
|
||||
|
||||
By effectively engaging with the Zed team and community early in your process, we're better positioned to give you feedback and understand your pull request once you open it. If the first thing we see from you is a big changeset, we're much less likely to respond to it in a timely manner.
|
||||
The internal advice for reviewers is as follows:
|
||||
|
||||
## Pair programming
|
||||
- If the fix/feature is obviously great, and the code is great. Hit merge.
|
||||
- If the fix/feature is obviously great, and the code is nearly great. Send PR comments, or offer to pair to get things perfect.
|
||||
- If the fix/feature is not obviously great, or the code needs rewriting from scratch. Close the PR with a thank you and some explanation.
|
||||
|
||||
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
|
||||
If you need more feedback from us: the best way is to be responsive to
|
||||
Github comments, or to offer up time to pair with us.
|
||||
|
||||
## Mandatory PR contents
|
||||
If you are making a larger change, or need advice on how to finish the change
|
||||
you're making, please open the PR early. We would love to help you get
|
||||
things right, and it's often easier to see how to solve a problem before the
|
||||
diff gets too big.
|
||||
|
||||
Please ensure the PR contains
|
||||
## Things we will (probably) not merge
|
||||
|
||||
- Before & after screenshots, if there are visual adjustments introduced.
|
||||
Although there are few hard and fast rules, typically we don't merge:
|
||||
|
||||
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
|
||||
|
||||
- A disclosure of the AI assistance usage, if any was used.
|
||||
|
||||
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
|
||||
|
||||
If the PR responses are being generated by an AI, disclose that as well.
|
||||
|
||||
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
|
||||
|
||||
## Tips to improve the chances of your PR getting reviewed and merged
|
||||
|
||||
- Discuss your plans ahead of time with the team
|
||||
- Small, focused, incremental pull requests are much easier to review
|
||||
- Spend time explaining your changes in the pull request body
|
||||
- Add test coverage and documentation
|
||||
- Choose tasks that align with our roadmap
|
||||
- Pair with us and watch us code to learn the codebase
|
||||
- Low effort PRs, such as those that just re-arrange syntax, won't be merged without a compelling justification
|
||||
|
||||
## File icons
|
||||
|
||||
Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner.
|
||||
|
||||
We do not accept PRs for file icons that are just an off-the-shelf SVG taken from somewhere else.
|
||||
|
||||
### Adding new icons to the Zed icon theme
|
||||
|
||||
If you would like to add a new icon to the Zed icon theme, [open a Discussion](https://github.com/zed-industries/zed/discussions/new?category=ux-and-design) and we can work with you on getting an icon designed and added to Zed.
|
||||
- Anything that can be provided by an extension. For example a new language, or theme. For adding themes or support for a new language to Zed, check out our [docs on developing extensions](https://zed.dev/docs/extensions/developing-extensions).
|
||||
- New file icons. Zed's default icon theme consists of icons that are hand-designed to fit together in a cohesive manner, please don't submit PRs with off-the-shelf SVGs.
|
||||
- Giant refactorings.
|
||||
- Non-trivial changes with no tests.
|
||||
- Features where (in our subjective opinion) the extra complexity isn't worth it for the number of people who will benefit.
|
||||
- Anything that seems completely AI generated.
|
||||
|
||||
## Bird's-eye view of Zed
|
||||
|
||||
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
We suggest you keep the [Zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
|
||||
1166
Cargo.lock
generated
1166
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
69
Cargo.toml
69
Cargo.toml
@@ -52,10 +52,12 @@ members = [
|
||||
"crates/debugger_tools",
|
||||
"crates/debugger_ui",
|
||||
"crates/deepseek",
|
||||
"crates/denoise",
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/edit_prediction",
|
||||
"crates/edit_prediction_button",
|
||||
"crates/edit_prediction_context",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/explorer_command_injector",
|
||||
@@ -94,9 +96,11 @@ members = [
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_models",
|
||||
"crates/language_onboarding",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
"crates/line_ending_selector",
|
||||
"crates/livekit_api",
|
||||
"crates/livekit_client",
|
||||
"crates/lmstudio",
|
||||
@@ -131,6 +135,7 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/scheduler",
|
||||
"crates/remote",
|
||||
"crates/remote_server",
|
||||
"crates/repl",
|
||||
@@ -141,7 +146,6 @@ members = [
|
||||
"crates/rules_library",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
@@ -193,6 +197,7 @@ members = [
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta_cli",
|
||||
"crates/zlog",
|
||||
@@ -209,7 +214,6 @@ members = [
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
@@ -275,6 +279,7 @@ context_server = { path = "crates/context_server" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
crashes = { path = "crates/crashes" }
|
||||
credentials_provider = { path = "crates/credentials_provider" }
|
||||
crossbeam = "0.8.4"
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
@@ -299,9 +304,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
|
||||
git_ui = { path = "crates/git_ui" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui", default-features = false, features = [
|
||||
"http_client",
|
||||
] }
|
||||
gpui = { path = "crates/gpui", default-features = false }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
@@ -311,6 +314,7 @@ icons = { path = "crates/icons" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
edit_prediction = { path = "crates/edit_prediction" }
|
||||
edit_prediction_button = { path = "crates/edit_prediction_button" }
|
||||
edit_prediction_context = { path = "crates/edit_prediction_context" }
|
||||
inspector_ui = { path = "crates/inspector_ui" }
|
||||
install_cli = { path = "crates/install_cli" }
|
||||
jj = { path = "crates/jj" }
|
||||
@@ -321,9 +325,11 @@ language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_onboarding = { path = "crates/language_onboarding" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
line_ending_selector = { path = "crates/line_ending_selector" }
|
||||
livekit_api = { path = "crates/livekit_api" }
|
||||
livekit_client = { path = "crates/livekit_client" }
|
||||
lmstudio = { path = "crates/lmstudio" }
|
||||
@@ -361,17 +367,17 @@ proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
rich_text = { path = "crates/rich_text" }
|
||||
rodio = { version = "0.21.1", default-features = false }
|
||||
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
|
||||
rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
@@ -422,6 +428,7 @@ worktree = { path = "crates/worktree" }
|
||||
x_ai = { path = "crates/x_ai" }
|
||||
zed = { path = "crates/zed" }
|
||||
zed_actions = { path = "crates/zed_actions" }
|
||||
zed_env_vars = { path = "crates/zed_env_vars" }
|
||||
zeta = { path = "crates/zeta" }
|
||||
zlog = { path = "crates/zlog" }
|
||||
zlog_settings = { path = "crates/zlog_settings" }
|
||||
@@ -430,7 +437,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.2.0-alpha.4", features = ["unstable"]}
|
||||
agent-client-protocol = { version = "0.2.1", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -444,6 +451,7 @@ async-fs = "2.1"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.29.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
@@ -456,16 +464,18 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
|
||||
] }
|
||||
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
|
||||
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
cargo_toml = "0.21"
|
||||
cfg-if = "1.0.3"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ciborium = "0.2"
|
||||
circular-buffer = "1.0"
|
||||
@@ -535,6 +545,31 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = [
|
||||
"NSArray",
|
||||
"NSAttributedString",
|
||||
"NSBundle",
|
||||
"NSCoder",
|
||||
"NSData",
|
||||
"NSDate",
|
||||
"NSDictionary",
|
||||
"NSEnumerator",
|
||||
"NSError",
|
||||
"NSGeometry",
|
||||
"NSNotification",
|
||||
"NSNull",
|
||||
"NSObjCRuntime",
|
||||
"NSObject",
|
||||
"NSProcessInfo",
|
||||
"NSRange",
|
||||
"NSRunLoop",
|
||||
"NSString",
|
||||
"NSURL",
|
||||
"NSUndoManager",
|
||||
"NSValue",
|
||||
"objc2-core-foundation",
|
||||
"std"
|
||||
] }
|
||||
open = "5.0.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
@@ -550,6 +585,7 @@ pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", re
|
||||
pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
portable-pty = "0.9.0"
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
|
||||
@@ -560,7 +596,7 @@ prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
rand = "0.9"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
@@ -585,9 +621,8 @@ rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
serde = { version = "1.0.221", features = ["derive", "rc"] }
|
||||
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
|
||||
serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
@@ -599,6 +634,7 @@ sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
simplelog = "0.12.2"
|
||||
slotmap = "1.0.6"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "2.0"
|
||||
sqlformat = "0.2"
|
||||
@@ -848,6 +884,9 @@ too_many_arguments = "allow"
|
||||
# We often have large enum variants yet we rarely actually bother with splitting them up.
|
||||
large_enum_variant = "allow"
|
||||
|
||||
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
|
||||
nonminimal_bool = "allow"
|
||||
|
||||
[workspace.metadata.cargo-machete]
|
||||
ignored = [
|
||||
"bindgen",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 29 KiB |
@@ -16,6 +16,7 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
@@ -63,8 +64,8 @@
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cut": "editor::Cut",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
@@ -246,7 +247,10 @@
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-d": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -327,6 +331,12 @@
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > ModeSelector",
|
||||
"bindings": {
|
||||
"ctrl-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
@@ -344,7 +354,8 @@
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -451,8 +462,8 @@
|
||||
"ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes",
|
||||
"back": "pane::GoBack",
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"forward": "pane::GoForward",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-alt-g": "search::SelectNextMatch",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"ctrl-alt-shift-g": "search::SelectPreviousMatch",
|
||||
@@ -485,8 +496,8 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
|
||||
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection
|
||||
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
@@ -582,7 +593,7 @@
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
@@ -627,6 +638,7 @@
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"ctrl-k ctrl-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
|
||||
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
|
||||
@@ -637,7 +649,9 @@
|
||||
"ctrl-k shift-up": "workspace::SwapPaneUp",
|
||||
"ctrl-k shift-down": "workspace::SwapPaneDown",
|
||||
"ctrl-shift-x": "zed::Extensions",
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
// All task parameters are captured and unchanged between reruns by default.
|
||||
// Use the `"reevaluate_context"` parameter to control this.
|
||||
"ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }],
|
||||
"ctrl-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn",
|
||||
@@ -1027,6 +1041,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"bindings": {
|
||||
@@ -1054,6 +1075,12 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "StashList || (StashList > Picker > Editor)",
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "stash_picker::DropStashItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"bindings": {
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
"cmd-k q": "editor::Rewrap",
|
||||
"cmd-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-delete": "editor::DeleteToEndOfLine",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cmd-x": "editor::Cut",
|
||||
"cmd-c": "editor::Copy",
|
||||
"cmd-v": "editor::Paste",
|
||||
@@ -218,7 +218,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !agent_diff",
|
||||
"context": "Editor && !agent_diff && !AgentPanel",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-z": "git::Restore",
|
||||
@@ -286,7 +286,10 @@
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-ctrl-b": "agent::ToggleBurnMode",
|
||||
"cmd-shift-enter": "agent::ContinueThread",
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-d": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -378,6 +381,12 @@
|
||||
"ctrl--": "pane::GoBack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > ModeSelector",
|
||||
"bindings": {
|
||||
"cmd-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
@@ -385,7 +394,8 @@
|
||||
"enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll"
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -395,7 +405,8 @@
|
||||
"cmd-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll"
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -536,8 +547,10 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"alt-shift-up": "editor::DuplicateLineUp",
|
||||
"alt-shift-down": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"cmd-ctrl-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
|
||||
"cmd-ctrl-right": "editor::SelectLargerSyntaxNode", // Expand selection
|
||||
"cmd-ctrl-up": "editor::SelectPreviousSyntaxNode", // Move selection up
|
||||
"cmd-ctrl-down": "editor::SelectNextSyntaxNode", // Move selection down
|
||||
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
@@ -649,7 +662,7 @@
|
||||
"alt-shift-enter": "toast::RunAction",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
@@ -690,6 +703,7 @@
|
||||
"cmd-?": "agent::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
"cmd-k cmd-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
|
||||
"cmd-k cmd-right": "workspace::ActivatePaneRight",
|
||||
@@ -710,7 +724,9 @@
|
||||
"bindings": {
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
// All task parameters are captured and unchanged between reruns by default.
|
||||
// Use the `"reevaluate_context"` parameter to control this.
|
||||
"cmd-alt-r": ["task::Rerun", { "reevaluate_context": false }],
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
@@ -1094,6 +1110,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1123,6 +1146,13 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "StashList || (StashList > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "stash_picker::DropStashItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"open": "workspace::Open",
|
||||
"ctrl-o": "workspace::Open",
|
||||
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -66,20 +65,15 @@
|
||||
"ctrl-k": "editor::CutToEndOfLine",
|
||||
"ctrl-k ctrl-q": "editor::Rewrap",
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cut": "editor::Cut",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"copy": "editor::Copy",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"paste": "editor::Paste",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"undo": "editor::Undo",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"redo": "editor::Redo",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
@@ -138,7 +132,6 @@
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl-shift-.": "assistant::QuoteSelection",
|
||||
@@ -177,7 +170,6 @@
|
||||
"context": "Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
@@ -225,7 +217,6 @@
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
@@ -258,7 +249,10 @@
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"super-ctrl-b": "agent::ToggleBurnMode",
|
||||
"alt-enter": "agent::ContinueWithBurnMode"
|
||||
"alt-enter": "agent::ContinueWithBurnMode",
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-d": "agent::RejectOnce"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -272,7 +266,6 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
@@ -346,6 +339,12 @@
|
||||
"enter": "agent::AcceptSuggestedContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > ModeSelector",
|
||||
"bindings": {
|
||||
"ctrl-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -353,7 +352,8 @@
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll"
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -367,7 +367,6 @@
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
}
|
||||
@@ -381,7 +380,6 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"find": "search::FocusSearch",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
@@ -408,7 +406,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"shift-find": "search::FocusSearch",
|
||||
"ctrl-shift-f": "search::FocusSearch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-r": "search::ToggleRegex" // vscode
|
||||
@@ -472,14 +469,12 @@
|
||||
"forward": "pane::GoForward",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPreviousMatch",
|
||||
"shift-find": "project_search::ToggleFocus",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"shift-alt-h": "search::ToggleReplace",
|
||||
"alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
"alt-find": "project_search::ToggleFilters",
|
||||
"alt-f": "project_search::ToggleFilters",
|
||||
"alt-r": "search::ToggleRegex",
|
||||
// "ctrl-shift-alt-x": "search::ToggleRegex",
|
||||
@@ -500,8 +495,10 @@
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
"shift-alt-up": "editor::DuplicateLineUp",
|
||||
"shift-alt-down": "editor::DuplicateLineDown",
|
||||
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection
|
||||
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
|
||||
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand selection
|
||||
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
|
||||
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version)
|
||||
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version)
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
@@ -579,27 +576,21 @@
|
||||
"context": "Workspace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
// Change to open path modal for existing remote connection by setting the parameter
|
||||
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
|
||||
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
"shift-alt-b": "branches::OpenRecent",
|
||||
"shift-alt-enter": "toast::RunAction",
|
||||
"ctrl-shift-`": "workspace::NewTerminal",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
|
||||
"shift-save": "workspace::SaveAs",
|
||||
"ctrl-shift-s": "workspace::SaveAs",
|
||||
"new": "workspace::NewFile",
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
@@ -621,7 +612,6 @@
|
||||
"shift-alt-0": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"shift-find": "pane::DeploySearch",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
@@ -641,9 +631,9 @@
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"ctrl-shift-d": "debug_panel::ToggleFocus",
|
||||
"ctrl-shift-/": "agent::ToggleFocus",
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-k s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"ctrl-m ctrl-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
|
||||
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
|
||||
@@ -654,7 +644,9 @@
|
||||
"ctrl-k shift-up": "workspace::SwapPaneUp",
|
||||
"ctrl-k shift-down": "workspace::SwapPaneDown",
|
||||
"ctrl-shift-x": "zed::Extensions",
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
// All task parameters are captured and unchanged between reruns by default.
|
||||
// Use the `"reevaluate_context"` parameter to control this.
|
||||
"ctrl-shift-r": ["task::Rerun", { "reevaluate_context": false }],
|
||||
"alt-t": "task::Rerun",
|
||||
"shift-alt-t": "task::Spawn",
|
||||
"shift-alt-r": ["task::Spawn", { "reveal_target": "center" }],
|
||||
@@ -848,9 +840,7 @@
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"alt-copy": "outline_panel::CopyPath",
|
||||
"shift-alt-c": "outline_panel::CopyPath",
|
||||
"shift-alt-copy": "workspace::CopyRelativePath",
|
||||
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
|
||||
"ctrl-alt-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
@@ -866,21 +856,14 @@
|
||||
"bindings": {
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"new": "project_panel::NewFile",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"alt-new": "project_panel::NewDirectory",
|
||||
"alt-n": "project_panel::NewDirectory",
|
||||
"cut": "project_panel::Cut",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"copy": "project_panel::Copy",
|
||||
"ctrl-insert": "project_panel::Copy",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"paste": "project_panel::Paste",
|
||||
"shift-insert": "project_panel::Paste",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"alt-copy": "project_panel::CopyPath",
|
||||
"shift-alt-c": "project_panel::CopyPath",
|
||||
"shift-alt-copy": "workspace::CopyRelativePath",
|
||||
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
|
||||
"enter": "project_panel::Rename",
|
||||
"f2": "project_panel::Rename",
|
||||
@@ -892,7 +875,6 @@
|
||||
"ctrl-alt-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
@@ -1075,6 +1057,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1105,15 +1094,20 @@
|
||||
"ctrl-backspace": "tab_switcher::CloseSelectedItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "StashList || (StashList > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "stash_picker::DropStashItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Terminal",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"copy": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"paste": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
@@ -1129,7 +1123,6 @@
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-shift-f": "buffer_search::Deploy",
|
||||
"ctrl-shift-l": "terminal::Clear",
|
||||
"ctrl-shift-w": "pane::CloseActiveItem",
|
||||
@@ -1210,7 +1203,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"ctrl-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"cmd-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-shift-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],
|
||||
|
||||
@@ -32,34 +32,6 @@
|
||||
"(": "vim::SentenceBackward",
|
||||
")": "vim::SentenceForward",
|
||||
"|": "vim::GoToColumn",
|
||||
"] ]": "vim::NextSectionStart",
|
||||
"] [": "vim::NextSectionEnd",
|
||||
"[ [": "vim::PreviousSectionStart",
|
||||
"[ ]": "vim::PreviousSectionEnd",
|
||||
"] m": "vim::NextMethodStart",
|
||||
"] shift-m": "vim::NextMethodEnd",
|
||||
"[ m": "vim::PreviousMethodStart",
|
||||
"[ shift-m": "vim::PreviousMethodEnd",
|
||||
"[ *": "vim::PreviousComment",
|
||||
"[ /": "vim::PreviousComment",
|
||||
"] *": "vim::NextComment",
|
||||
"] /": "vim::NextComment",
|
||||
"[ -": "vim::PreviousLesserIndent",
|
||||
"[ +": "vim::PreviousGreaterIndent",
|
||||
"[ =": "vim::PreviousSameIndent",
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
"[ e": "editor::MoveLineUp",
|
||||
"] e": "editor::MoveLineDown",
|
||||
"[ f": "workspace::FollowNextCollaborator",
|
||||
"] f": "workspace::FollowNextCollaborator",
|
||||
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
@@ -83,10 +55,6 @@
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPreviousMatch",
|
||||
"%": "vim::Matching",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
||||
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
|
||||
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
|
||||
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
|
||||
@@ -219,6 +187,46 @@
|
||||
".": "vim::Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
|
||||
"bindings": {
|
||||
"] ]": "vim::NextSectionStart",
|
||||
"] [": "vim::NextSectionEnd",
|
||||
"[ [": "vim::PreviousSectionStart",
|
||||
"[ ]": "vim::PreviousSectionEnd",
|
||||
"] m": "vim::NextMethodStart",
|
||||
"] shift-m": "vim::NextMethodEnd",
|
||||
"[ m": "vim::PreviousMethodStart",
|
||||
"[ shift-m": "vim::PreviousMethodEnd",
|
||||
"[ *": "vim::PreviousComment",
|
||||
"[ /": "vim::PreviousComment",
|
||||
"] *": "vim::NextComment",
|
||||
"] /": "vim::NextComment",
|
||||
"[ -": "vim::PreviousLesserIndent",
|
||||
"[ +": "vim::PreviousGreaterIndent",
|
||||
"[ =": "vim::PreviousSameIndent",
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
"[ e": "editor::MoveLineUp",
|
||||
"] e": "editor::MoveLineDown",
|
||||
"[ f": "workspace::FollowNextCollaborator",
|
||||
"] f": "workspace::FollowNextCollaborator",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
@@ -249,9 +257,6 @@
|
||||
"g w": "vim::PushRewrap",
|
||||
"g q": "vim::PushRewrap",
|
||||
"insert": "vim::InsertBefore",
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
@@ -317,10 +322,28 @@
|
||||
"g w": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
// "g ?": "vim::ConvertToRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
"\"": "vim::PushRegister"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == helix_select",
|
||||
"bindings": {
|
||||
"v": "vim::NormalBefore",
|
||||
";": "vim::HelixCollapseSelection",
|
||||
"~": "vim::ChangeCase",
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"shift-j": "vim::JoinLines",
|
||||
"i": "vim::InsertBefore",
|
||||
"a": "vim::InsertAfter",
|
||||
"p": "vim::Paste",
|
||||
"u": "vim::Undo",
|
||||
"r": "vim::PushReplace",
|
||||
"s": "vim::Substitute",
|
||||
"ctrl-pageup": "pane::ActivatePreviousItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
".": "vim::Repeat",
|
||||
"alt-.": "vim::RepeatFind"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -337,7 +360,7 @@
|
||||
"ctrl-x ctrl-z": "editor::Cancel",
|
||||
"ctrl-x ctrl-e": "vim::LineDown",
|
||||
"ctrl-x ctrl-y": "vim::LineUp",
|
||||
"ctrl-w": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-u": "editor::DeleteToBeginningOfLine",
|
||||
"ctrl-t": "vim::Indent",
|
||||
"ctrl-d": "vim::Outdent",
|
||||
@@ -394,9 +417,17 @@
|
||||
"bindings": {
|
||||
"i": "vim::HelixInsert",
|
||||
"a": "vim::HelixAppend",
|
||||
"ctrl-[": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
|
||||
"bindings": {
|
||||
";": "vim::HelixCollapseSelection",
|
||||
":": "command_palette::Toggle",
|
||||
"m": "vim::PushHelixMatch",
|
||||
"]": ["vim::PushHelixNext", { "around": true }],
|
||||
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||
"left": "vim::WrappingLeft",
|
||||
"right": "vim::WrappingRight",
|
||||
"h": "vim::WrappingLeft",
|
||||
@@ -419,13 +450,6 @@
|
||||
"insert": "vim::InsertBefore",
|
||||
"alt-.": "vim::RepeatFind",
|
||||
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPreviousHunk",
|
||||
// Goto mode
|
||||
"g n": "pane::ActivateNextItem",
|
||||
"g p": "pane::ActivatePreviousItem",
|
||||
@@ -469,9 +493,6 @@
|
||||
"space c": "editor::ToggleComments",
|
||||
"space y": "editor::Copy",
|
||||
"space p": "editor::Paste",
|
||||
// Match mode
|
||||
"m m": "vim::Matching",
|
||||
"m i w": ["workspace::SendKeystrokes", "v i w"],
|
||||
"shift-u": "editor::Redo",
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
@@ -540,7 +561,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
|
||||
@@ -577,6 +598,48 @@
|
||||
"e": "vim::EntireFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_m",
|
||||
"bindings": {
|
||||
"m": "vim::Matching"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_next",
|
||||
"bindings": {
|
||||
"z": "vim::NextSectionStart",
|
||||
"shift-z": "vim::NextSectionEnd",
|
||||
"*": "vim::NextComment",
|
||||
"/": "vim::NextComment",
|
||||
"-": "vim::NextLesserIndent",
|
||||
"+": "vim::NextGreaterIndent",
|
||||
"=": "vim::NextSameIndent",
|
||||
"b": "pane::ActivateNextItem",
|
||||
"shift-b": "pane::ActivateLastItem",
|
||||
"x": "editor::SelectSmallerSyntaxNode",
|
||||
"d": "editor::GoToDiagnostic",
|
||||
"c": "editor::GoToHunk",
|
||||
"space": "vim::InsertEmptyLineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_previous",
|
||||
"bindings": {
|
||||
"z": "vim::PreviousSectionStart",
|
||||
"shift-z": "vim::PreviousSectionEnd",
|
||||
"*": "vim::PreviousComment",
|
||||
"/": "vim::PreviousComment",
|
||||
"-": "vim::PreviousLesserIndent",
|
||||
"+": "vim::PreviousGreaterIndent",
|
||||
"=": "vim::PreviousSameIndent",
|
||||
"b": "pane::ActivatePreviousItem",
|
||||
"shift-b": ["pane::ActivateItem", 0],
|
||||
"x": "editor::SelectLargerSyntaxNode",
|
||||
"d": "editor::GoToPreviousDiagnostic",
|
||||
"c": "editor::GoToPreviousHunk",
|
||||
"space": "vim::InsertEmptyLineAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
"bindings": {
|
||||
@@ -823,11 +886,11 @@
|
||||
"j": "menu::SelectNext",
|
||||
"k": "menu::SelectPrevious",
|
||||
"l": "project_panel::ExpandSelectedEntry",
|
||||
"o": "project_panel::OpenPermanent",
|
||||
"shift-d": "project_panel::Delete",
|
||||
"shift-r": "project_panel::Rename",
|
||||
"t": "project_panel::OpenPermanent",
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"v": "project_panel::OpenSplitVertical",
|
||||
"o": "project_panel::OpenSplitHorizontal",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"s": "workspace::OpenWithSystem",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"project_name": null,
|
||||
// The name of the Zed theme to use for the UI.
|
||||
//
|
||||
// `mode` is one of:
|
||||
@@ -361,6 +362,11 @@
|
||||
// - It is adjacent to an edge (start or end)
|
||||
// - It is adjacent to a whitespace (left or right)
|
||||
"show_whitespaces": "selection",
|
||||
// Visible characters used to render whitespace when show_whitespaces is enabled.
|
||||
"whitespace_map": {
|
||||
"space": "•",
|
||||
"tab": "→"
|
||||
},
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone live by default
|
||||
@@ -385,6 +391,8 @@
|
||||
"use_system_window_tabs": false,
|
||||
// Titlebar related settings
|
||||
"title_bar": {
|
||||
// When to show the title bar: "always" | "never" | "hide_in_full_screen".
|
||||
"show": "always",
|
||||
// Whether to show the branch icon beside branch switcher in the titlebar.
|
||||
"show_branch_icon": false,
|
||||
// Whether to show the branch name button in the titlebar.
|
||||
@@ -740,16 +748,6 @@
|
||||
// Default width of the collaboration panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"chat_panel": {
|
||||
// When to show the chat panel button in the status bar.
|
||||
// Can be 'never', 'always', or 'when_in_call',
|
||||
// or a boolean (interpreted as 'never'/'always').
|
||||
"button": "when_in_call",
|
||||
// Where to dock the chat panel. Can be 'left' or 'right'.
|
||||
"dock": "right",
|
||||
// Default width of the chat panel.
|
||||
"default_width": 240
|
||||
},
|
||||
"git_panel": {
|
||||
// Whether to show the git panel button in the status bar.
|
||||
"button": true,
|
||||
@@ -838,6 +836,9 @@
|
||||
// }
|
||||
],
|
||||
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
|
||||
//
|
||||
// Note: This setting has no effect on external agents that support permission modes, such as Claude Code.
|
||||
// You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests.
|
||||
"always_allow_tool_actions": false,
|
||||
// When enabled, the agent will stream edits.
|
||||
"stream_edits": false,
|
||||
@@ -913,7 +914,11 @@
|
||||
/// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
|
||||
///
|
||||
/// Default: true
|
||||
"expand_terminal_card": true
|
||||
"expand_terminal_card": true,
|
||||
// Minimum number of lines to display in the agent message editor.
|
||||
//
|
||||
// Default: 4
|
||||
"message_editor_min_lines": 4
|
||||
},
|
||||
// The settings for slash commands.
|
||||
"slash_commands": {
|
||||
@@ -962,7 +967,7 @@
|
||||
// Show git status colors in the editor tabs.
|
||||
"git_status": false,
|
||||
// Position of the close button on the editor tabs.
|
||||
// One of: ["right", "left", "hidden"]
|
||||
// One of: ["right", "left"]
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false,
|
||||
@@ -1205,6 +1210,10 @@
|
||||
// The minimum column number to show the inline blame information at
|
||||
"min_column": 0
|
||||
},
|
||||
// Control which information is shown in the branch picker.
|
||||
"branch_picker": {
|
||||
"show_author_name": true
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
@@ -1689,6 +1698,11 @@
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
"formatter": {
|
||||
"language_server": {
|
||||
"name": "ruff"
|
||||
}
|
||||
},
|
||||
"debuggers": ["Debugpy"]
|
||||
},
|
||||
"Ruby": {
|
||||
@@ -1834,6 +1848,15 @@
|
||||
// "typescript": "deno"
|
||||
// }
|
||||
},
|
||||
// REPL settings.
|
||||
"repl": {
|
||||
// Maximum number of columns to keep in REPL's scrollback buffer.
|
||||
// Clamped with [20, 512] range.
|
||||
"max_columns": 128,
|
||||
// Maximum number of lines to keep in REPL's scrollback buffer.
|
||||
// Clamped with [4, 256] range.
|
||||
"max_lines": 32
|
||||
},
|
||||
// Vim settings
|
||||
"vim": {
|
||||
"default_mode": "normal",
|
||||
|
||||
@@ -316,6 +316,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#a6a5a0ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#d2a6ffff",
|
||||
"font_style": null,
|
||||
@@ -702,6 +707,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#73777bff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#a37accff",
|
||||
"font_style": null,
|
||||
@@ -1088,6 +1098,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#b4b3aeff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#dfbfffff",
|
||||
"font_style": null,
|
||||
|
||||
@@ -325,6 +325,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#e5d5adff",
|
||||
"font_style": null,
|
||||
@@ -725,6 +730,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#e5d5adff",
|
||||
"font_style": null,
|
||||
@@ -1125,6 +1135,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#83a598ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#e5d5adff",
|
||||
"font_style": null,
|
||||
@@ -1525,6 +1540,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#413d3aff",
|
||||
"font_style": null,
|
||||
@@ -1925,6 +1945,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#413d3aff",
|
||||
"font_style": null,
|
||||
@@ -2325,6 +2350,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#066578ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#413d3aff",
|
||||
"font_style": null,
|
||||
|
||||
@@ -321,6 +321,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#d07277ff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#b1574bff",
|
||||
"font_style": null,
|
||||
@@ -715,6 +720,11 @@
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.markup": {
|
||||
"color": "#d3604fff",
|
||||
"font_style": null,
|
||||
"font_weight": null
|
||||
},
|
||||
"punctuation.special": {
|
||||
"color": "#b92b46ff",
|
||||
"font_style": null,
|
||||
|
||||
@@ -18,8 +18,8 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
|
||||
[dependencies]
|
||||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
anyhow.workspace = true
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
@@ -45,7 +45,6 @@ url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -7,12 +7,12 @@ use agent_settings::AgentSettings;
|
||||
use collections::HashSet;
|
||||
pub use connection::*;
|
||||
pub use diff::*;
|
||||
use futures::future::Shared;
|
||||
use language::language_settings::FormatOnSave;
|
||||
pub use mention::*;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use task::{Shell, ShellBuilder};
|
||||
pub use terminal::*;
|
||||
|
||||
use action_log::ActionLog;
|
||||
@@ -34,7 +34,7 @@ use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use ui::App;
|
||||
use util::{ResultExt, get_system_shell};
|
||||
use util::{ResultExt, get_default_system_shell};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -785,9 +785,7 @@ pub struct AcpThread {
|
||||
session_id: acp::SessionId,
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
available_commands: Vec<acp::AvailableCommand>,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
determine_shell: Shared<Task<String>>,
|
||||
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
}
|
||||
|
||||
@@ -804,6 +802,9 @@ pub enum AcpThreadEvent {
|
||||
Error,
|
||||
LoadError(LoadError),
|
||||
PromptCapabilitiesUpdated,
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
ModeUpdated(acp::SessionModeId),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -811,7 +812,6 @@ impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum ThreadStatus {
|
||||
Idle,
|
||||
WaitingForToolConfirmation,
|
||||
Generating,
|
||||
}
|
||||
|
||||
@@ -859,10 +859,9 @@ impl AcpThread {
|
||||
action_log: Entity<ActionLog>,
|
||||
session_id: acp::SessionId,
|
||||
mut prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
|
||||
available_commands: Vec<acp::AvailableCommand>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = *prompt_capabilities_rx.borrow();
|
||||
let prompt_capabilities = prompt_capabilities_rx.borrow().clone();
|
||||
let task = cx.spawn::<_, anyhow::Result<()>>(async move |this, cx| {
|
||||
loop {
|
||||
let caps = prompt_capabilities_rx.recv().await?;
|
||||
@@ -873,20 +872,6 @@ impl AcpThread {
|
||||
}
|
||||
});
|
||||
|
||||
let determine_shell = cx
|
||||
.background_spawn(async move {
|
||||
if cfg!(windows) {
|
||||
return get_system_shell();
|
||||
}
|
||||
|
||||
if which::which("bash").is_ok() {
|
||||
"bash".into()
|
||||
} else {
|
||||
get_system_shell()
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
|
||||
Self {
|
||||
action_log,
|
||||
shared_buffers: Default::default(),
|
||||
@@ -899,19 +884,13 @@ impl AcpThread {
|
||||
session_id,
|
||||
token_usage: None,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
_observe_prompt_capabilities: task,
|
||||
terminals: HashMap::default(),
|
||||
determine_shell,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> acp::PromptCapabilities {
|
||||
self.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn available_commands(&self) -> Vec<acp::AvailableCommand> {
|
||||
self.available_commands.clone()
|
||||
self.prompt_capabilities.clone()
|
||||
}
|
||||
|
||||
pub fn connection(&self) -> &Rc<dyn AgentConnection> {
|
||||
@@ -940,11 +919,7 @@ impl AcpThread {
|
||||
|
||||
pub fn status(&self) -> ThreadStatus {
|
||||
if self.send_task.is_some() {
|
||||
if self.waiting_for_tool_confirmation() {
|
||||
ThreadStatus::WaitingForToolConfirmation
|
||||
} else {
|
||||
ThreadStatus::Generating
|
||||
}
|
||||
ThreadStatus::Generating
|
||||
} else {
|
||||
ThreadStatus::Idle
|
||||
}
|
||||
@@ -1009,6 +984,12 @@ impl AcpThread {
|
||||
acp::SessionUpdate::Plan(plan) => {
|
||||
self.update_plan(plan, cx);
|
||||
}
|
||||
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
|
||||
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
|
||||
}
|
||||
acp::SessionUpdate::CurrentModeUpdate { current_mode_id } => {
|
||||
cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1130,9 +1111,33 @@ impl AcpThread {
|
||||
let update = update.into();
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
|
||||
let ix = self
|
||||
.index_for_tool_call(update.id())
|
||||
.context("Tool call not found")?;
|
||||
let ix = match self.index_for_tool_call(update.id()) {
|
||||
Some(ix) => ix,
|
||||
None => {
|
||||
// Tool call not found - create a failed tool call entry
|
||||
let failed_tool_call = ToolCall {
|
||||
id: update.id().clone(),
|
||||
label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
|
||||
kind: acp::ToolKind::Fetch,
|
||||
content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Tool call not found".to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
&languages,
|
||||
cx,
|
||||
))],
|
||||
status: ToolCallStatus::Failed,
|
||||
locations: Vec::new(),
|
||||
resolved_locations: Vec::new(),
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
};
|
||||
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -1305,11 +1310,12 @@ impl AcpThread {
|
||||
&mut self,
|
||||
tool_call: acp::ToolCallUpdate,
|
||||
options: Vec<acp::PermissionOption>,
|
||||
respect_always_allow_setting: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<BoxFuture<'static, acp::RequestPermissionOutcome>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
if AgentSettings::get_global(cx).always_allow_tool_actions {
|
||||
if respect_always_allow_setting && AgentSettings::get_global(cx).always_allow_tool_actions {
|
||||
// Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
|
||||
// some tools would (incorrectly) continue to auto-accept.
|
||||
if let Some(allow_once_option) = options.iter().find_map(|option| {
|
||||
@@ -1379,26 +1385,27 @@ impl AcpThread {
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
}
|
||||
|
||||
/// Returns true if the last turn is awaiting tool authorization
|
||||
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||
pub fn first_tool_awaiting_confirmation(&self) -> Option<&ToolCall> {
|
||||
let mut first_tool_call = None;
|
||||
|
||||
for entry in self.entries.iter().rev() {
|
||||
match &entry {
|
||||
AgentThreadEntry::ToolCall(call) => match call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||
ToolCallStatus::Pending
|
||||
| ToolCallStatus::InProgress
|
||||
| ToolCallStatus::Completed
|
||||
| ToolCallStatus::Failed
|
||||
| ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled => continue,
|
||||
},
|
||||
AgentThreadEntry::ToolCall(call) => {
|
||||
if let ToolCallStatus::WaitingForConfirmation { .. } = call.status {
|
||||
first_tool_call = Some(call);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
|
||||
// Reached the beginning of the turn
|
||||
return false;
|
||||
// Reached the beginning of the turn.
|
||||
// If we had pending permission requests in the previous turn, they have been cancelled.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
first_tool_call
|
||||
}
|
||||
|
||||
pub fn plan(&self) -> &Plan {
|
||||
@@ -1447,6 +1454,7 @@ impl AcpThread {
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: message.to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
@@ -1465,6 +1473,7 @@ impl AcpThread {
|
||||
let request = acp::PromptRequest {
|
||||
prompt: message.clone(),
|
||||
session_id: self.session_id.clone(),
|
||||
meta: None,
|
||||
};
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
@@ -1556,7 +1565,8 @@ impl AcpThread {
|
||||
let canceled = matches!(
|
||||
result,
|
||||
Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
meta: None,
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -1569,15 +1579,43 @@ impl AcpThread {
|
||||
this.send_task.take();
|
||||
}
|
||||
|
||||
// Truncate entries if the last prompt was refused.
|
||||
// Handle refusal - distinguish between user prompt and tool call refusals
|
||||
if let Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: _,
|
||||
})) = result
|
||||
&& let Some((ix, _)) = this.last_user_message()
|
||||
{
|
||||
let range = ix..this.entries.len();
|
||||
this.entries.truncate(ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
if let Some((user_msg_ix, _)) = this.last_user_message() {
|
||||
// Check if there's a completed tool call with results after the last user message
|
||||
// This indicates the refusal is in response to tool output, not the user's prompt
|
||||
let has_completed_tool_call_after_user_msg =
|
||||
this.entries.iter().skip(user_msg_ix + 1).any(|entry| {
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = entry {
|
||||
// Check if the tool call has completed and has output
|
||||
matches!(tool_call.status, ToolCallStatus::Completed)
|
||||
&& tool_call.raw_output.is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if has_completed_tool_call_after_user_msg {
|
||||
// Refusal is due to tool output - don't truncate, just notify
|
||||
// The model refused based on what the tool returned
|
||||
cx.emit(AcpThreadEvent::Refusal);
|
||||
} else {
|
||||
// User prompt was refused - truncate back to before the user message
|
||||
let range = user_msg_ix..this.entries.len();
|
||||
if range.start < range.end {
|
||||
this.entries.truncate(user_msg_ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
}
|
||||
cx.emit(AcpThreadEvent::Refusal);
|
||||
}
|
||||
} else {
|
||||
// No user message found, treat as general refusal
|
||||
cx.emit(AcpThreadEvent::Refusal);
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(AcpThreadEvent::Stopped);
|
||||
@@ -1615,13 +1653,13 @@ impl AcpThread {
|
||||
cx.foreground_executor().spawn(send_task)
|
||||
}
|
||||
|
||||
/// Rewinds this thread to before the entry at `index`, removing it and all
|
||||
/// subsequent entries while reverting any changes made from that point.
|
||||
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("not supported")));
|
||||
};
|
||||
let Some(message) = self.user_message(&id) else {
|
||||
/// Restores the git working tree to the state at the given checkpoint (if one exists)
|
||||
pub fn restore_checkpoint(
|
||||
&mut self,
|
||||
id: UserMessageId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some((_, message)) = self.user_message_mut(&id) else {
|
||||
return Task::ready(Err(anyhow!("message not found")));
|
||||
};
|
||||
|
||||
@@ -1629,15 +1667,30 @@ impl AcpThread {
|
||||
.checkpoint
|
||||
.as_ref()
|
||||
.map(|c| c.git_checkpoint.clone());
|
||||
|
||||
let rewind = self.rewind(id.clone(), cx);
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
rewind.await?;
|
||||
if let Some(checkpoint) = checkpoint {
|
||||
git_store
|
||||
.update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Rewinds this thread to before the entry at `index`, removing it and all
|
||||
/// subsequent entries while rejecting any action_log changes made from that point.
|
||||
/// Unlike `restore_checkpoint`, this method does not restore from git.
|
||||
pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let Some(truncate) = self.connection.truncate(&self.session_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("not supported")));
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
|
||||
this.update(cx, |this, cx| {
|
||||
if let Some((ix, _)) = this.user_message_mut(&id) {
|
||||
@@ -1645,7 +1698,11 @@ impl AcpThread {
|
||||
this.entries.truncate(ix);
|
||||
cx.emit(AcpThreadEvent::EntriesRemoved(range));
|
||||
}
|
||||
})
|
||||
this.action_log()
|
||||
.update(cx, |action_log, cx| action_log.reject_all_edits(cx))
|
||||
})?
|
||||
.await;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1702,20 +1759,6 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
fn user_message(&self, id: &UserMessageId) -> Option<&UserMessage> {
|
||||
self.entries.iter().find_map(|entry| {
|
||||
if let AgentThreadEntry::UserMessage(message) = entry {
|
||||
if message.id.as_ref() == Some(id) {
|
||||
Some(message)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> {
|
||||
self.entries.iter_mut().enumerate().find_map(|(ix, entry)| {
|
||||
if let AgentThreadEntry::UserMessage(message) = entry {
|
||||
@@ -1905,28 +1948,13 @@ impl AcpThread {
|
||||
|
||||
pub fn create_terminal(
|
||||
&self,
|
||||
mut command: String,
|
||||
command: String,
|
||||
args: Vec<String>,
|
||||
extra_env: Vec<acp::EnvVariable>,
|
||||
cwd: Option<PathBuf>,
|
||||
output_byte_limit: Option<u64>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Terminal>>> {
|
||||
for arg in args {
|
||||
command.push(' ');
|
||||
command.push_str(&arg);
|
||||
}
|
||||
|
||||
let shell_command = if cfg!(windows) {
|
||||
format!("$null | & {{{}}}", command.replace("\"", "'"))
|
||||
} else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) {
|
||||
// Make sure once we're *inside* the shell, we cd into `cwd`
|
||||
format!("(cd {cwd}; {}) </dev/null", command)
|
||||
} else {
|
||||
format!("({}) </dev/null", command)
|
||||
};
|
||||
let args = vec!["-c".into(), shell_command];
|
||||
|
||||
let env = match &cwd {
|
||||
Some(dir) => self.project.update(cx, |project, cx| {
|
||||
project.directory_environment(dir.as_path().into(), cx)
|
||||
@@ -1947,20 +1975,30 @@ impl AcpThread {
|
||||
|
||||
let project = self.project.clone();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let determine_shell = self.determine_shell.clone();
|
||||
|
||||
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
|
||||
let terminal_task = cx.spawn({
|
||||
let terminal_id = terminal_id.clone();
|
||||
async move |_this, cx| {
|
||||
let program = determine_shell.await;
|
||||
let env = env.await;
|
||||
let (command, args) = ShellBuilder::new(
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
.remote_client()
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})?
|
||||
.as_deref(),
|
||||
&Shell::Program(get_default_system_shell()),
|
||||
)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command), &args);
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_task(
|
||||
task::SpawnInTerminal {
|
||||
command: Some(program),
|
||||
args,
|
||||
command: Some(command.clone()),
|
||||
args: args.clone(),
|
||||
cwd: cwd.clone(),
|
||||
env,
|
||||
..Default::default()
|
||||
@@ -1973,7 +2011,7 @@ impl AcpThread {
|
||||
cx.new(|cx| {
|
||||
Terminal::new(
|
||||
terminal_id,
|
||||
command,
|
||||
&format!("{} {}", command, args.join(" ")),
|
||||
cwd,
|
||||
output_byte_limit.map(|l| l as usize),
|
||||
terminal,
|
||||
@@ -2089,7 +2127,7 @@ mod tests {
|
||||
use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Fs};
|
||||
use rand::Rng as _;
|
||||
use rand::{distr, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
@@ -2132,6 +2170,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Hello, ".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -2155,6 +2194,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "world!".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -2176,6 +2216,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Assistant response".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
@@ -2189,6 +2230,7 @@ mod tests {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "New user message".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -2234,6 +2276,7 @@ mod tests {
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2304,6 +2347,7 @@ mod tests {
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2372,6 +2416,7 @@ mod tests {
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2380,6 +2425,7 @@ mod tests {
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2428,6 +2474,7 @@ mod tests {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2470,11 +2517,13 @@ mod tests {
|
||||
path: "/test/test.txt".into(),
|
||||
old_text: None,
|
||||
new_text: "foo".into(),
|
||||
meta: None,
|
||||
},
|
||||
}],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
@@ -2483,6 +2532,7 @@ mod tests {
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2545,6 +2595,7 @@ mod tests {
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2659,7 +2710,7 @@ mod tests {
|
||||
let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else {
|
||||
panic!("unexpected entries {:?}", thread.entries)
|
||||
};
|
||||
thread.rewind(message.id.clone().unwrap(), cx)
|
||||
thread.restore_checkpoint(message.id.clone().unwrap(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -2681,6 +2732,193 @@ mod tests {
|
||||
assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tool_result_refusal(cx: &mut TestAppContext) {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
|
||||
// Create a connection that simulates refusal after tool result
|
||||
let prompt_count = Arc::new(AtomicUsize::new(0));
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
|
||||
let prompt_count = prompt_count.clone();
|
||||
move |_request, thread, mut cx| {
|
||||
let count = prompt_count.fetch_add(1, SeqCst);
|
||||
async move {
|
||||
if count == 0 {
|
||||
// First prompt: Generate a tool call with result
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::ToolCall(acp::ToolCall {
|
||||
id: acp::ToolCallId("tool1".into()),
|
||||
title: "Test Tool".into(),
|
||||
kind: acp::ToolKind::Fetch,
|
||||
status: acp::ToolCallStatus::Completed,
|
||||
content: vec![],
|
||||
locations: vec![],
|
||||
raw_input: Some(serde_json::json!({"query": "test"})),
|
||||
raw_output: Some(
|
||||
serde_json::json!({"result": "inappropriate content"}),
|
||||
),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
// Now return refusal because of the tool result
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Track if we see a Refusal event
|
||||
let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
|
||||
let saw_refusal_event_captured = saw_refusal_event.clone();
|
||||
thread.update(cx, |_thread, cx| {
|
||||
cx.subscribe(
|
||||
&thread,
|
||||
move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
|
||||
if matches!(event, AcpThreadEvent::Refusal) {
|
||||
*saw_refusal_event_captured.lock().unwrap() = true;
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Send a user message - this will trigger tool call and then refusal
|
||||
let send_task = thread.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Hello".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.background_executor.spawn(send_task).detach();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Verify that:
|
||||
// 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt)
|
||||
// 2. The user message was NOT truncated
|
||||
assert!(
|
||||
*saw_refusal_event.lock().unwrap(),
|
||||
"Refusal event should be emitted for tool result refusals"
|
||||
);
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
let entries = thread.entries();
|
||||
assert!(entries.len() >= 2, "Should have user message and tool call");
|
||||
|
||||
// Verify user message is still there
|
||||
assert!(
|
||||
matches!(entries[0], AgentThreadEntry::UserMessage(_)),
|
||||
"User message should not be truncated"
|
||||
);
|
||||
|
||||
// Verify tool call is there with result
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] {
|
||||
assert!(
|
||||
tool_call.raw_output.is_some(),
|
||||
"Tool call should have output"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected tool call at index 1");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
|
||||
let refuse_next = Arc::new(AtomicBool::new(false));
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
|
||||
let refuse_next = refuse_next.clone();
|
||||
move |_request, _thread, _cx| {
|
||||
if refuse_next.load(SeqCst) {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
} else {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Track if we see a Refusal event
|
||||
let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
|
||||
let saw_refusal_event_captured = saw_refusal_event.clone();
|
||||
thread.update(cx, |_thread, cx| {
|
||||
cx.subscribe(
|
||||
&thread,
|
||||
move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
|
||||
if matches!(event, AcpThreadEvent::Refusal) {
|
||||
*saw_refusal_event_captured.lock().unwrap() = true;
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
|
||||
// Send a message that will be refused
|
||||
refuse_next.store(true, SeqCst);
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that a Refusal event WAS emitted for user prompt refusal
|
||||
assert!(
|
||||
*saw_refusal_event.lock().unwrap(),
|
||||
"Refusal event should be emitted for user prompt refusals"
|
||||
);
|
||||
|
||||
// Verify the message was truncated (user prompt refusal)
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
assert_eq!(thread.to_markdown(cx), "");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_refusal(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2697,6 +2935,7 @@ mod tests {
|
||||
if refuse_next.load(SeqCst) {
|
||||
return Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2715,6 +2954,7 @@ mod tests {
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -2744,8 +2984,8 @@ mod tests {
|
||||
);
|
||||
});
|
||||
|
||||
// Simulate refusing the second message, ensuring the conversation gets
|
||||
// truncated to before sending it.
|
||||
// Simulate refusing the second message. The message should be truncated
|
||||
// when a user prompt is refused.
|
||||
refuse_next.store(true, SeqCst);
|
||||
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx)))
|
||||
.await
|
||||
@@ -2851,8 +3091,8 @@ mod tests {
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
let session_id = acp::SessionId(
|
||||
rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
rand::rng()
|
||||
.sample_iter(&distr::Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
@@ -2870,8 +3110,8 @@ mod tests {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -2902,6 +3142,7 @@ mod tests {
|
||||
} else {
|
||||
Task::ready(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -2943,4 +3184,65 @@ mod tests {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tool_call_not_found_creates_failed_entry(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let connection = Rc::new(FakeAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to update a tool call that doesn't exist
|
||||
let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
|
||||
thread.update(cx, |thread, cx| {
|
||||
let result = thread.handle_session_update(
|
||||
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
|
||||
id: nonexistent_id.clone(),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
// The update should succeed (not return an error)
|
||||
assert!(result.is_ok());
|
||||
|
||||
// There should now be exactly one entry in the thread
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
|
||||
// The entry should be a failed tool call
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] {
|
||||
assert_eq!(tool_call.id, nonexistent_id);
|
||||
assert!(matches!(tool_call.status, ToolCallStatus::Failed));
|
||||
assert_eq!(tool_call.kind, acp::ToolKind::Fetch);
|
||||
|
||||
// Check that the content contains the error message
|
||||
assert_eq!(tool_call.content.len(), 1);
|
||||
if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] {
|
||||
match content_block {
|
||||
ContentBlock::Markdown { markdown } => {
|
||||
let markdown_text = markdown.read(cx).source();
|
||||
assert!(markdown_text.contains("Tool call not found"));
|
||||
}
|
||||
ContentBlock::Empty => panic!("Expected markdown content, got empty"),
|
||||
ContentBlock::ResourceLink { .. } => {
|
||||
panic!("Expected markdown content, got resource link")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]);
|
||||
}
|
||||
} else {
|
||||
panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,15 @@ pub trait AgentConnection {
|
||||
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn session_modes(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionModes>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
@@ -109,6 +118,14 @@ pub trait AgentTelemetry {
|
||||
) -> Task<Result<serde_json::Value>>;
|
||||
}
|
||||
|
||||
pub trait AgentSessionModes {
|
||||
fn current_mode(&self) -> acp::SessionModeId;
|
||||
|
||||
fn all_modes(&self) -> Vec<acp::SessionMode>;
|
||||
|
||||
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthRequired {
|
||||
pub description: Option<String>,
|
||||
@@ -337,8 +354,8 @@ mod test_support {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -377,7 +394,10 @@ mod test_support {
|
||||
response_tx.replace(tx);
|
||||
cx.spawn(async move |_| {
|
||||
let stop_reason = rx.await?;
|
||||
Ok(acp::PromptResponse { stop_reason })
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
for update in self.next_prompt_updates.lock().drain(..) {
|
||||
@@ -398,6 +418,7 @@ mod test_support {
|
||||
thread.request_tool_call_authorization(
|
||||
tool_call.clone().into(),
|
||||
options.clone(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})??
|
||||
@@ -415,6 +436,7 @@ mod test_support {
|
||||
try_join_all(tasks).await?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ impl MentionUri {
|
||||
FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
}
|
||||
MentionUri::PastedImage => IconName::Image.path().into(),
|
||||
MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
|
||||
MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct TerminalOutput {
|
||||
impl Terminal {
|
||||
pub fn new(
|
||||
id: acp::TerminalId,
|
||||
command: String,
|
||||
command_label: &str,
|
||||
working_dir: Option<PathBuf>,
|
||||
output_byte_limit: Option<usize>,
|
||||
terminal: Entity<terminal::Terminal>,
|
||||
@@ -40,7 +40,7 @@ impl Terminal {
|
||||
id,
|
||||
command: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
format!("```\n{}\n```", command).into(),
|
||||
format!("```\n{}\n```", command_label).into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
@@ -75,6 +75,7 @@ impl Terminal {
|
||||
acp::TerminalExitStatus {
|
||||
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
|
||||
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
|
||||
meta: None,
|
||||
}
|
||||
})
|
||||
.shared(),
|
||||
@@ -105,7 +106,9 @@ impl Terminal {
|
||||
exit_status: Some(acp::TerminalExitStatus {
|
||||
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
|
||||
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
}
|
||||
} else {
|
||||
let (current_content, original_len) = self.truncated_output(cx);
|
||||
@@ -114,6 +117,7 @@ impl Terminal {
|
||||
truncated: current_content.len() < original_len,
|
||||
output: current_content,
|
||||
exit_status: None,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2218,7 +2218,7 @@ mod tests {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0..25 => {
|
||||
action_log.update(cx, |log, cx| {
|
||||
let range = buffer.read(cx).random_byte_range(0, &mut rng);
|
||||
@@ -2237,7 +2237,7 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
_ => {
|
||||
let is_agent_edit = rng.gen_bool(0.5);
|
||||
let is_agent_edit = rng.random_bool(0.5);
|
||||
if is_agent_edit {
|
||||
log::info!("agent edit");
|
||||
} else {
|
||||
@@ -2252,7 +2252,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
quiesce(&action_log, &buffer, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType};
|
||||
use editor::Editor;
|
||||
use extension_host::{ExtensionOperation, ExtensionStore};
|
||||
use futures::StreamExt;
|
||||
@@ -84,7 +84,6 @@ impl ActivityIndicator {
|
||||
) -> Entity<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let workspace_handle = cx.entity();
|
||||
let this = cx.new(|cx| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -102,20 +101,6 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe_in(
|
||||
&workspace_handle,
|
||||
window,
|
||||
|activity_indicator, _, event, window, cx| {
|
||||
if let workspace::Event::ClearActivityIndicator = event
|
||||
&& activity_indicator.statuses.pop().is_some()
|
||||
{
|
||||
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|activity_indicator, _, event, cx| {
|
||||
@@ -227,7 +212,8 @@ impl ActivityIndicator {
|
||||
server_name,
|
||||
status,
|
||||
} => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let create_buffer =
|
||||
project.update(cx, |project, cx| project.create_buffer(false, cx));
|
||||
let status = status.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
@@ -294,18 +280,13 @@ impl ActivityIndicator {
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss_error_message(
|
||||
&mut self,
|
||||
_: &DismissErrorMessage,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let error_dismissed = if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss_error(cx))
|
||||
fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context<Self>) {
|
||||
let dismissed = if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| updater.dismiss(cx))
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if error_dismissed {
|
||||
if dismissed {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -527,7 +508,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.statuses
|
||||
.retain(|status| !downloading.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
@@ -556,7 +537,7 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(move |this, window, cx| {
|
||||
this.statuses
|
||||
.retain(|status| !checking_for_update.contains(&status.name));
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
@@ -664,13 +645,14 @@ impl ActivityIndicator {
|
||||
.and_then(|updater| match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_rotate_animation(3)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
}),
|
||||
@@ -682,19 +664,20 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(version)),
|
||||
}),
|
||||
AutoUpdateStatus::Installing { version } => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_rotate_animation(3)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
this.dismiss_message(&DismissMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: Some(Self::version_tooltip_message(version)),
|
||||
}),
|
||||
@@ -704,17 +687,18 @@ impl ActivityIndicator {
|
||||
on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
|
||||
tooltip_message: Some(Self::version_tooltip_message(version)),
|
||||
}),
|
||||
AutoUpdateStatus::Errored => Some(Content {
|
||||
AutoUpdateStatus::Errored { error } => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: "Auto update failed".to_string(),
|
||||
message: "Failed to update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
window.dispatch_action(Box::new(workspace::OpenLog), cx);
|
||||
this.dismiss_message(&DismissMessage, window, cx);
|
||||
})),
|
||||
tooltip_message: None,
|
||||
tooltip_message: Some(format!("{error}")),
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
})
|
||||
@@ -752,7 +736,7 @@ impl ActivityIndicator {
|
||||
})),
|
||||
message,
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&Default::default(), window, cx)
|
||||
this.dismiss_message(&Default::default(), window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
})
|
||||
@@ -791,7 +775,7 @@ impl Render for ActivityIndicator {
|
||||
let result = h_flex()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
.on_action(cx.listener(Self::dismiss_message));
|
||||
let Some(content) = self.content_to_render(cx) else {
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ time.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -6,7 +6,7 @@ use futures::future;
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
|
||||
use icons::IconName;
|
||||
use language::{Buffer, ParseStatus};
|
||||
use language::Buffer;
|
||||
use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
|
||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
@@ -191,46 +191,19 @@ impl FileContextHandle {
|
||||
let buffer = self.buffer.clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
// For large files, use outline instead of full content
|
||||
if rope.len() > outline::AUTO_OUTLINE_SIZE {
|
||||
// Wait until the buffer has been fully parsed, so we can read its outline
|
||||
if let Ok(mut parse_status) =
|
||||
buffer.read_with(cx, |buffer, _| buffer.parse_status())
|
||||
{
|
||||
while *parse_status.borrow() != ParseStatus::Idle {
|
||||
parse_status.changed().await.log_err();
|
||||
}
|
||||
let buffer_content =
|
||||
outline::get_buffer_content_or_outline(buffer.clone(), Some(&full_path), &cx)
|
||||
.await
|
||||
.unwrap_or_else(|_| outline::BufferContent {
|
||||
text: rope.to_string(),
|
||||
is_outline: false,
|
||||
});
|
||||
|
||||
if let Ok(snapshot) = buffer.read_with(cx, |buffer, _| buffer.snapshot())
|
||||
&& let Some(outline) = snapshot.outline(None)
|
||||
{
|
||||
let items = outline
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.to_point(&snapshot));
|
||||
|
||||
if let Ok(outline_text) =
|
||||
outline::render_outline(items, None, 0, usize::MAX).await
|
||||
{
|
||||
let context = AgentContext::File(FileContext {
|
||||
handle: self,
|
||||
full_path,
|
||||
text: outline_text.into(),
|
||||
is_outline: true,
|
||||
});
|
||||
return Some((context, vec![buffer]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to full content if we couldn't build an outline
|
||||
// (or didn't need to because the file was small enough)
|
||||
let context = AgentContext::File(FileContext {
|
||||
handle: self,
|
||||
full_path,
|
||||
text: rope.to_string().into(),
|
||||
is_outline: false,
|
||||
text: buffer_content.text.into(),
|
||||
is_outline: buffer_content.is_outline,
|
||||
});
|
||||
Some((context, vec![buffer]))
|
||||
})
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
use crate::{
|
||||
ThreadId,
|
||||
thread_store::{SerializedThreadMetadata, ThreadStore},
|
||||
};
|
||||
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_context::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -61,7 +58,6 @@ enum SerializedRecentOpen {
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
@@ -70,15 +66,11 @@ pub struct HistoryStore {
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
thread_store: Entity<ThreadStore>,
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&thread_store, |_, _, cx| cx.notify()),
|
||||
cx.observe(&context_store, |_, _, cx| cx.notify()),
|
||||
];
|
||||
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
|
||||
@@ -96,7 +88,6 @@ impl HistoryStore {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
thread_store,
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
@@ -112,13 +103,6 @@ impl HistoryStore {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
history_entries.extend(
|
||||
self.thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.cloned()
|
||||
.map(HistoryEntry::Thread),
|
||||
);
|
||||
history_entries.extend(
|
||||
self.context_store
|
||||
.read(cx)
|
||||
@@ -141,22 +125,6 @@ impl HistoryStore {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let thread_entries = self
|
||||
.thread_store
|
||||
.read(cx)
|
||||
.reverse_chronological_threads()
|
||||
.flat_map(|thread| {
|
||||
self.recently_opened_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, entry)| match entry {
|
||||
HistoryEntryId::Thread(id) if &thread.id == id => {
|
||||
Some((index, HistoryEntry::Thread(thread.clone())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
let context_entries =
|
||||
self.context_store
|
||||
.read(cx)
|
||||
@@ -173,8 +141,7 @@ impl HistoryStore {
|
||||
})
|
||||
});
|
||||
|
||||
thread_entries
|
||||
.chain(context_entries)
|
||||
context_entries
|
||||
// optimization to halt iteration early
|
||||
.take(self.recently_opened_entries.len())
|
||||
.sorted_unstable_by_key(|(index, _)| *index)
|
||||
|
||||
@@ -41,8 +41,7 @@ use std::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
|
||||
@@ -68,6 +68,7 @@ uuid.workspace = true
|
||||
watch.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -166,33 +166,41 @@ impl LanguageModels {
|
||||
cx.background_spawn(async move {
|
||||
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
|
||||
if let Err(err) = authenticate_task.await {
|
||||
if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
} else {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
match err {
|
||||
language_model::AuthenticateError::CredentialsNotFound => {
|
||||
// Since we're authenticating these providers in the
|
||||
// background for the purposes of populating the
|
||||
// language selector, we don't care about providers
|
||||
// where the credentials are not found.
|
||||
}
|
||||
language_model::AuthenticateError::ConnectionRefused => {
|
||||
// Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures.
|
||||
// LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it.
|
||||
// TODO: Better manage LM Studio auth logic to avoid these noisy failures.
|
||||
}
|
||||
_ => {
|
||||
// Some providers have noisy failure states that we
|
||||
// don't want to spam the logs with every time the
|
||||
// language model selector is initialized.
|
||||
//
|
||||
// Ideally these should have more clear failure modes
|
||||
// that we know are safe to ignore here, like what we do
|
||||
// with `CredentialsNotFound` above.
|
||||
match provider_id.0.as_ref() {
|
||||
"lmstudio" | "ollama" => {
|
||||
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
|
||||
//
|
||||
// These fail noisily, so we don't log them.
|
||||
}
|
||||
"copilot_chat" => {
|
||||
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
|
||||
}
|
||||
_ => {
|
||||
log::error!(
|
||||
"Failed to authenticate provider: {}: {err}",
|
||||
provider_name.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +300,6 @@ impl NativeAgent {
|
||||
action_log.clone(),
|
||||
session_id.clone(),
|
||||
prompt_capabilities_rx,
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -748,6 +755,7 @@ impl NativeAgentConnection {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
@@ -760,6 +768,7 @@ impl NativeAgentConnection {
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
true,
|
||||
cx,
|
||||
@@ -772,7 +781,9 @@ impl NativeAgentConnection {
|
||||
response,
|
||||
}) => {
|
||||
let outcome_task = acp_thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(tool_call, options, cx)
|
||||
thread.request_tool_call_authorization(
|
||||
tool_call, options, true, cx,
|
||||
)
|
||||
})??;
|
||||
cx.background_spawn(async move {
|
||||
if let acp::RequestPermissionOutcome::Selected { option_id } =
|
||||
@@ -803,7 +814,10 @@ impl NativeAgentConnection {
|
||||
}
|
||||
ThreadEvent::Stop(stop_reason) => {
|
||||
log::debug!("Assistant message complete: {:?}", stop_reason);
|
||||
return Ok(acp::PromptResponse { stop_reason });
|
||||
return Ok(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -817,6 +831,7 @@ impl NativeAgentConnection {
|
||||
log::debug!("Response stream completed");
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1440,6 +1455,7 @@ mod tests {
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
" mean?".into(),
|
||||
],
|
||||
|
||||
@@ -18,6 +18,7 @@ use sqlez::{
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use ui::{App, SharedString};
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
pub type DbMessage = crate::Message;
|
||||
pub type DbSummary = DetailedSummaryState;
|
||||
@@ -201,9 +202,6 @@ impl DbThread {
|
||||
}
|
||||
}
|
||||
|
||||
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
|
||||
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DataType {
|
||||
#[serde(rename = "json")]
|
||||
@@ -430,7 +428,9 @@ mod tests {
|
||||
use http_client::FakeHttpClient;
|
||||
use language_model::Role;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::test::TempTree;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
env_logger::try_init().ok();
|
||||
@@ -451,6 +451,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_retrieving_old_thread(cx: &mut TestAppContext) {
|
||||
let tree = TempTree::new(json!({}));
|
||||
util::paths::set_home_dir(tree.path().into());
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
@@ -35,10 +35,15 @@ impl AgentServer for NativeAgentServer {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
_root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
||||
) -> Task<
|
||||
Result<(
|
||||
Rc<dyn acp_thread::AgentConnection>,
|
||||
Option<task::SpawnInTerminal>,
|
||||
)>,
|
||||
> {
|
||||
log::debug!(
|
||||
"NativeAgentServer::connect called for path: {:?}",
|
||||
_root_dir
|
||||
@@ -60,7 +65,10 @@ impl AgentServer for NativeAgentServer {
|
||||
let connection = NativeAgentConnection(agent);
|
||||
log::debug!("NativeAgentServer connection established successfully");
|
||||
|
||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||
Ok((
|
||||
Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>,
|
||||
None,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_thinking(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1300,6 +1299,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..
|
||||
},
|
||||
meta: None,
|
||||
},
|
||||
)) if Some(&id) == echo_id.as_ref() => {
|
||||
echo_completed = true;
|
||||
@@ -1349,7 +1349,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1688,7 +1687,6 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
|
||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
@@ -1929,6 +1927,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
acp::PromptRequest {
|
||||
session_id: session_id.clone(),
|
||||
prompt: vec!["ghi".into()],
|
||||
meta: None,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -1993,6 +1992,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
locations: vec![],
|
||||
raw_input: Some(json!({})),
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2006,6 +2006,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
raw_input: Some(json!({ "content": "Thinking hard!" })),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2017,6 +2018,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2028,6 +2030,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
content: Some(vec!["Thinking hard!".into()]),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
@@ -2040,6 +2043,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
|
||||
raw_output: Some("Finished thinking.".into()),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -2353,15 +2357,20 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
||||
settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
agent_settings::init(cx);
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
match model {
|
||||
TestModel::Fake => {}
|
||||
TestModel::Sonnet4 => {
|
||||
gpui_tokio::init(cx);
|
||||
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
|
||||
cx.set_http_client(Arc::new(http_client));
|
||||
client::init_settings(cx);
|
||||
let client = Client::production(cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client.clone(), cx);
|
||||
}
|
||||
};
|
||||
|
||||
watch_settings(fs.clone(), cx);
|
||||
});
|
||||
@@ -2475,6 +2484,7 @@ fn setup_context_server(
|
||||
path: "somebinary".into(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
timeout: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -24,7 +24,11 @@ impl AgentTool for EchoTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Echo".into()
|
||||
}
|
||||
|
||||
@@ -55,7 +59,11 @@ impl AgentTool for DelayTool {
|
||||
"delay"
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Delay {}ms", input.ms).into()
|
||||
} else {
|
||||
@@ -100,7 +108,11 @@ impl AgentTool for ToolRequiringPermission {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"This tool requires permission".into()
|
||||
}
|
||||
|
||||
@@ -135,7 +147,11 @@ impl AgentTool for InfiniteTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Infinite Tool".into()
|
||||
}
|
||||
|
||||
@@ -186,7 +202,11 @@ impl AgentTool for WordListTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"List of random words".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -614,6 +614,7 @@ impl Thread {
|
||||
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
|
||||
let image = model.map_or(true, |model| model.supports_images());
|
||||
acp::PromptCapabilities {
|
||||
meta: None,
|
||||
image,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
@@ -728,6 +729,7 @@ impl Thread {
|
||||
stream
|
||||
.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(tool_use.id.to_string().into()),
|
||||
title: tool_use.name.to_string(),
|
||||
kind: acp::ToolKind::Other,
|
||||
@@ -741,7 +743,7 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
let title = tool.initial_title(tool_use.input.clone());
|
||||
let title = tool.initial_title(tool_use.input.clone(), cx);
|
||||
let kind = tool.kind();
|
||||
stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone());
|
||||
|
||||
@@ -1062,7 +1064,11 @@ impl Thread {
|
||||
self.action_log.clone(),
|
||||
));
|
||||
self.add_tool(DiagnosticsTool::new(self.project.clone()));
|
||||
self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry));
|
||||
self.add_tool(EditFileTool::new(
|
||||
self.project.clone(),
|
||||
cx.weak_entity(),
|
||||
language_registry,
|
||||
));
|
||||
self.add_tool(FetchTool::new(self.project.read(cx).client().http_client()));
|
||||
self.add_tool(FindPathTool::new(self.project.clone()));
|
||||
self.add_tool(GrepTool::new(self.project.clone()));
|
||||
@@ -1514,7 +1520,7 @@ impl Thread {
|
||||
let mut title = SharedString::from(&tool_use.name);
|
||||
let mut kind = acp::ToolKind::Other;
|
||||
if let Some(tool) = tool.as_ref() {
|
||||
title = tool.initial_title(tool_use.input.clone());
|
||||
title = tool.initial_title(tool_use.input.clone(), cx);
|
||||
kind = tool.kind();
|
||||
}
|
||||
|
||||
@@ -2148,7 +2154,11 @@ where
|
||||
fn kind() -> acp::ToolKind;
|
||||
|
||||
/// The initial tool title to display. Can be updated during the tool run.
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString;
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString;
|
||||
|
||||
/// Returns the JSON schema that describes the tool's input.
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema {
|
||||
@@ -2196,7 +2206,7 @@ pub trait AnyAgentTool {
|
||||
fn name(&self) -> SharedString;
|
||||
fn description(&self) -> SharedString;
|
||||
fn kind(&self) -> acp::ToolKind;
|
||||
fn initial_title(&self, input: serde_json::Value) -> SharedString;
|
||||
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString;
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
||||
fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool {
|
||||
true
|
||||
@@ -2232,9 +2242,9 @@ where
|
||||
T::kind()
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: serde_json::Value) -> SharedString {
|
||||
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString {
|
||||
let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input);
|
||||
self.0.initial_title(parsed_input)
|
||||
self.0.initial_title(parsed_input, _cx)
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
@@ -2325,6 +2335,7 @@ impl ThreadEventStream {
|
||||
input: serde_json::Value,
|
||||
) -> acp::ToolCall {
|
||||
acp::ToolCall {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(id.to_string().into()),
|
||||
title,
|
||||
kind,
|
||||
@@ -2344,6 +2355,7 @@ impl ThreadEventStream {
|
||||
self.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
|
||||
acp::ToolCallUpdate {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(tool_use_id.to_string().into()),
|
||||
fields,
|
||||
}
|
||||
@@ -2429,6 +2441,7 @@ impl ToolCallEventStream {
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
|
||||
ToolCallAuthorization {
|
||||
tool_call: acp::ToolCallUpdate {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
title: Some(title.into()),
|
||||
@@ -2440,16 +2453,19 @@ impl ToolCallEventStream {
|
||||
id: acp::PermissionOptionId("always_allow".into()),
|
||||
name: "Always Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowAlways,
|
||||
meta: None,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: acp::PermissionOptionId("allow".into()),
|
||||
name: "Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowOnce,
|
||||
meta: None,
|
||||
},
|
||||
acp::PermissionOption {
|
||||
id: acp::PermissionOptionId("deny".into()),
|
||||
name: "Deny".into(),
|
||||
kind: acp::PermissionOptionKind::RejectOnce,
|
||||
meta: None,
|
||||
},
|
||||
],
|
||||
response: response_tx,
|
||||
@@ -2603,17 +2619,21 @@ impl From<UserMessageContent> for acp::ContentBlock {
|
||||
UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
|
||||
data: image.source.to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
meta: None,
|
||||
annotations: None,
|
||||
uri: None,
|
||||
}),
|
||||
UserMessageContent::Mention { uri, content } => {
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource {
|
||||
meta: None,
|
||||
resource: acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents {
|
||||
meta: None,
|
||||
mime_type: None,
|
||||
text: content,
|
||||
uri: uri.to_uri().to_string(),
|
||||
|
||||
@@ -145,7 +145,7 @@ impl AnyAgentTool for ContextServerTool {
|
||||
ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
|
||||
fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString {
|
||||
format!("Run MCP tool `{}`", self.tool.name).into()
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ impl AnyAgentTool for ContextServerTool {
|
||||
return Task::ready(Err(anyhow!("Context server not found")));
|
||||
};
|
||||
let tool_name = self.tool.name.clone();
|
||||
let authorize = event_stream.authorize(self.initial_title(input.clone()), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx);
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
authorize.await?;
|
||||
|
||||
@@ -58,7 +58,11 @@ impl AgentTool for CopyPathTool {
|
||||
ToolKind::Move
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> ui::SharedString {
|
||||
if let Ok(input) = input {
|
||||
let src = MarkdownInlineCode(&input.source_path);
|
||||
let dest = MarkdownInlineCode(&input.destination_path);
|
||||
|
||||
@@ -49,7 +49,11 @@ impl AgentTool for CreateDirectoryTool {
|
||||
ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
|
||||
} else {
|
||||
|
||||
@@ -52,7 +52,11 @@ impl AgentTool for DeletePathTool {
|
||||
ToolKind::Delete
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Delete “`{}`”", input.path).into()
|
||||
} else {
|
||||
|
||||
@@ -71,7 +71,11 @@ impl AgentTool for DiagnosticsTool {
|
||||
acp::ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Some(path) = input.ok().and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(path),
|
||||
_ => None,
|
||||
|
||||
@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[schemars(inline)]
|
||||
pub enum EditFileMode {
|
||||
Edit,
|
||||
Create,
|
||||
@@ -119,11 +120,17 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
|
||||
pub struct EditFileTool {
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
impl EditFileTool {
|
||||
pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
thread,
|
||||
language_registry,
|
||||
}
|
||||
@@ -194,22 +201,50 @@ impl AgentTool for EditFileTool {
|
||||
acp::ToolKind::Edit
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString {
|
||||
match input {
|
||||
Ok(input) => input.display_description.into(),
|
||||
Ok(input) => self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&input.path, cx)
|
||||
.and_then(|project_path| {
|
||||
self.project
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into(),
|
||||
Err(raw_input) => {
|
||||
if let Some(input) =
|
||||
serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
|
||||
{
|
||||
let path = input.path.trim();
|
||||
if !path.is_empty() {
|
||||
return self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&input.path, cx)
|
||||
.and_then(|project_path| {
|
||||
self.project
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into();
|
||||
}
|
||||
|
||||
let description = input.display_description.trim();
|
||||
if !description.is_empty() {
|
||||
return description.to_string().into();
|
||||
}
|
||||
|
||||
let path = input.path.trim().to_string();
|
||||
if !path.is_empty() {
|
||||
return path.into();
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_UI_TEXT.into()
|
||||
@@ -239,6 +274,7 @@ impl AgentTool for EditFileTool {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: None,
|
||||
meta: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -318,7 +354,7 @@ impl AgentTool for EditFileTool {
|
||||
}).ok();
|
||||
if let Some(abs_path) = abs_path.clone() {
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
|
||||
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -544,7 +580,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -559,11 +595,12 @@ mod tests {
|
||||
path: "root/nonexistent_file.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
};
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
Arc::new(EditFileTool::new(
|
||||
project,
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
@@ -742,7 +779,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -774,6 +811,7 @@ mod tests {
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
))
|
||||
@@ -832,11 +870,12 @@ mod tests {
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
|
||||
// Stream the unformatted content
|
||||
@@ -884,7 +923,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -917,6 +956,7 @@ mod tests {
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
))
|
||||
@@ -968,11 +1008,12 @@ mod tests {
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
|
||||
// Stream the content with trailing whitespace
|
||||
@@ -1011,7 +1052,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -1019,7 +1060,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
|
||||
// Test 1: Path with .zed component should require confirmation
|
||||
@@ -1147,7 +1192,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -1155,7 +1200,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test global config paths - these should require confirmation if they exist and are outside the project
|
||||
let test_cases = vec![
|
||||
@@ -1263,7 +1312,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test files in different worktrees
|
||||
let test_cases = vec![
|
||||
@@ -1343,7 +1396,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test edge cases
|
||||
let test_cases = vec![
|
||||
@@ -1426,7 +1483,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test different EditFileMode values
|
||||
let modes = vec![
|
||||
@@ -1506,48 +1567,67 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project,
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
"src/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
"Fix error handling"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
"Fix error handling"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(serde_json::Value::Null)),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
cx.update(|cx| {
|
||||
// ...
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
"src/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
"Fix error handling"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
"src/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(serde_json::Value::Null), cx),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1574,7 +1654,11 @@ mod tests {
|
||||
|
||||
// Ensure the diff is finalized after the edit completes.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
@@ -1599,7 +1683,11 @@ mod tests {
|
||||
// Ensure the diff is finalized if an error occurs while editing.
|
||||
{
|
||||
model.forbid_requests();
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
@@ -1622,7 +1710,11 @@ mod tests {
|
||||
|
||||
// Ensure the diff is finalized if the tool call gets dropped.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
|
||||
@@ -126,7 +126,11 @@ impl AgentTool for FetchTool {
|
||||
acp::ToolKind::Fetch
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
match input {
|
||||
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
|
||||
Err(_) => "Fetch URL".into(),
|
||||
|
||||
@@ -93,7 +93,11 @@ impl AgentTool for FindPathTool {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
let mut title = "Find paths".to_string();
|
||||
if let Ok(input) = input {
|
||||
title.push_str(&format!(" matching “`{}`”", input.glob));
|
||||
@@ -134,6 +138,7 @@ impl AgentTool for FindPathTool {
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -75,7 +75,11 @@ impl AgentTool for GrepTool {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
match input {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
@@ -257,10 +261,8 @@ impl AgentTool for GrepTool {
|
||||
let end_row = range.end.row;
|
||||
output.push_str("\n### ");
|
||||
|
||||
if let Some(parent_symbols) = &parent_symbols {
|
||||
for symbol in parent_symbols {
|
||||
write!(output, "{} › ", symbol.text)?;
|
||||
}
|
||||
for symbol in parent_symbols {
|
||||
write!(output, "{} › ", symbol.text)?;
|
||||
}
|
||||
|
||||
if range.start.row == end_row {
|
||||
|
||||
@@ -59,7 +59,11 @@ impl AgentTool for ListDirectoryTool {
|
||||
ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
format!("List the {path} directory's contents").into()
|
||||
|
||||
@@ -60,7 +60,11 @@ impl AgentTool for MovePathTool {
|
||||
ToolKind::Move
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let src = MarkdownInlineCode(&input.source_path);
|
||||
let dest = MarkdownInlineCode(&input.destination_path);
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[schemars(inline)]
|
||||
pub enum Timezone {
|
||||
/// Use UTC for the datetime.
|
||||
Utc,
|
||||
@@ -40,7 +41,11 @@ impl AgentTool for NowTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Get current time".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,11 @@ impl AgentTool for OpenTool {
|
||||
ToolKind::Execute
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
|
||||
} else {
|
||||
@@ -61,7 +65,7 @@ impl AgentTool for OpenTool {
|
||||
) -> Task<Result<Self::Output>> {
|
||||
// If path_or_url turns out to be a path in the project, make it absolute.
|
||||
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
|
||||
cx.background_spawn(async move {
|
||||
authorize.await?;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
@@ -68,13 +68,31 @@ impl AgentTool for ReadFileTool {
|
||||
acp::ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
input
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|input| Path::new(&input.path).file_name())
|
||||
.map(|file_name| file_name.to_string_lossy().to_string().into())
|
||||
.unwrap_or_default()
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input
|
||||
&& let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx)
|
||||
&& let Some(path) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
{
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!("Read file `{}` (from line {})", path.display(), start)
|
||||
}
|
||||
_ => format!("Read file `{}`", path.display()),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
"Read file".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -86,6 +104,12 @@ impl AgentTool for ReadFileTool {
|
||||
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
|
||||
};
|
||||
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Failed to convert {} to absolute path",
|
||||
&input.path
|
||||
)));
|
||||
};
|
||||
|
||||
// Error out if this path is either excluded or private in global settings
|
||||
let global_settings = WorktreeSettings::get_global(cx);
|
||||
@@ -121,6 +145,15 @@ impl AgentTool for ReadFileTool {
|
||||
|
||||
let file_path = input.path.clone();
|
||||
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path.clone(),
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
meta: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if image_store::is_image_file(&self.project, &project_path, cx) {
|
||||
return cx.spawn(async move |cx| {
|
||||
let image_entity: Entity<ImageItem> = cx
|
||||
@@ -193,70 +226,53 @@ impl AgentTool for ReadFileTool {
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// No line ranges specified, so check file size to see if it's too big.
|
||||
let file_size = buffer.read_with(cx, |buffer, _cx| buffer.text().len())?;
|
||||
let buffer_content =
|
||||
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), cx)
|
||||
.await?;
|
||||
|
||||
if file_size <= outline::AUTO_OUTLINE_SIZE {
|
||||
// File is small enough, so return its contents.
|
||||
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_read(buffer.clone(), cx);
|
||||
})?;
|
||||
|
||||
Ok(result.into())
|
||||
} else {
|
||||
// File is too big, so return the outline
|
||||
// and a suggestion to read again with line numbers.
|
||||
let outline =
|
||||
outline::file_outline(project.clone(), file_path, action_log, None, cx)
|
||||
.await?;
|
||||
if buffer_content.is_outline {
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once.
|
||||
|
||||
Here is an outline of its symbols:
|
||||
|
||||
{outline}
|
||||
{}
|
||||
|
||||
Using the line numbers in this outline, you can call this tool again
|
||||
while specifying the start_line and end_line fields to see the
|
||||
implementations of symbols in the outline.
|
||||
|
||||
Alternatively, you can fall back to the `grep` tool (if available)
|
||||
to search the file for specific content."
|
||||
to search the file for specific content.", buffer_content.text
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
Ok(buffer_content.text.into())
|
||||
}
|
||||
};
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
if let Some(abs_path) = project.absolute_path(&project_path, cx) {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor.unwrap_or(text::Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor.unwrap_or(text::Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||
let markdown = MarkdownCodeBlock {
|
||||
tag: &input.path,
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||
let markdown = MarkdownCodeBlock {
|
||||
tag: &input.path,
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ impl AgentTool for TerminalTool {
|
||||
acp::ToolKind::Execute
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let mut lines = input.command.lines();
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
@@ -93,7 +97,7 @@ impl AgentTool for TerminalTool {
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
|
||||
cx.spawn(async move |cx| {
|
||||
authorize.await?;
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ impl AgentTool for ThinkingTool {
|
||||
acp::ToolKind::Think
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Thinking".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ impl AgentTool for WebSearchTool {
|
||||
acp::ToolKind::Fetch
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Searching the Web".into()
|
||||
}
|
||||
|
||||
@@ -118,6 +122,7 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
|
||||
mime_type: None,
|
||||
annotations: None,
|
||||
size: None,
|
||||
meta: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -23,34 +23,31 @@ action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
client = { workspace = true, optional = true }
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
gpui_tokio = { workspace = true, optional = true }
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
reqwest_client = { workspace = true, optional = true }
|
||||
schemars.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
watch.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::AgentServerCommand;
|
||||
use acp_thread::AgentConnection;
|
||||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
@@ -8,10 +7,13 @@ use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use std::{path::Path, rc::Rc, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
@@ -29,6 +31,11 @@ pub struct AcpConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
root_dir: PathBuf,
|
||||
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
||||
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
||||
child: smol::process::Child,
|
||||
_io_task: Task<Result<()>>,
|
||||
_wait_task: Task<Result<()>>,
|
||||
_stderr_task: Task<Result<()>>,
|
||||
@@ -37,15 +44,26 @@ pub struct AcpConnection {
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
|
||||
let conn = AcpConnection::stdio(
|
||||
server_name,
|
||||
command.clone(),
|
||||
root_dir,
|
||||
default_mode,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
}
|
||||
|
||||
@@ -56,17 +74,21 @@ impl AcpConnection {
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(command.path)
|
||||
let mut child = util::command::new_smol_command(command.path);
|
||||
child
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
.stderr(std::process::Stdio::piped());
|
||||
if !is_remote {
|
||||
child.current_dir(root_dir);
|
||||
}
|
||||
let mut child = child.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
@@ -102,8 +124,9 @@ impl AcpConnection {
|
||||
|
||||
let wait_task = cx.spawn({
|
||||
let sessions = sessions.clone();
|
||||
let status_fut = child.status();
|
||||
async move |cx| {
|
||||
let status = child.status().await?;
|
||||
let status = status_fut.await?;
|
||||
|
||||
for session in sessions.borrow().values() {
|
||||
session
|
||||
@@ -133,9 +156,12 @@ impl AcpConnection {
|
||||
fs: acp::FileSystemCapability {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
meta: None,
|
||||
},
|
||||
terminal: true,
|
||||
meta: None,
|
||||
},
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -145,19 +171,33 @@ impl AcpConnection {
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
root_dir: root_dir.to_owned(),
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
agent_capabilities: response.agent_capabilities,
|
||||
default_mode,
|
||||
_io_task: io_task,
|
||||
_wait_task: wait_task,
|
||||
_stderr_task: stderr_task,
|
||||
child,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
|
||||
&self.agent_capabilities.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn root_dir(&self) -> &Path {
|
||||
&self.root_dir
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AcpConnection {
|
||||
fn drop(&mut self) {
|
||||
// See the comment on the child field.
|
||||
self.child.kill().log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
@@ -167,37 +207,47 @@ impl AgentConnection for AcpConnection {
|
||||
cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<AcpThread>>> {
|
||||
let name = self.server_name.clone();
|
||||
let conn = self.connection.clone();
|
||||
let sessions = self.sessions.clone();
|
||||
let default_mode = self.default_mode.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
let mcp_servers = if project.read(cx).is_local() {
|
||||
context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer::Stdio {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
} else {
|
||||
// In SSH projects, the external agent is running on the remote
|
||||
// machine, and currently we only run MCP servers on the local
|
||||
// machine. So don't pass any MCP servers to the agent in that case.
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
|
||||
.new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
@@ -213,6 +263,54 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
})?;
|
||||
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
|
||||
if let Some(default_mode) = default_mode {
|
||||
if let Some(modes) = modes.as_ref() {
|
||||
let mut modes_ref = modes.borrow_mut();
|
||||
let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode);
|
||||
|
||||
if has_mode {
|
||||
let initial_mode_id = modes_ref.current_mode_id.clone();
|
||||
|
||||
cx.spawn({
|
||||
let default_mode = default_mode.clone();
|
||||
let session_id = response.session_id.clone();
|
||||
let modes = modes.clone();
|
||||
async move |_| {
|
||||
let result = conn.set_session_mode(acp::SetSessionModeRequest {
|
||||
session_id,
|
||||
mode_id: default_mode,
|
||||
meta: None,
|
||||
})
|
||||
.await.log_err();
|
||||
|
||||
if result.is_none() {
|
||||
modes.borrow_mut().current_mode_id = initial_mode_id;
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
modes_ref.current_mode_id = default_mode;
|
||||
} else {
|
||||
let available_modes = modes_ref
|
||||
.available_modes
|
||||
.iter()
|
||||
.map(|mode| format!("- `{}`: {}", mode.id, mode.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
log::warn!(
|
||||
"`{default_mode}` is not valid {name} mode. Available options:\n{available_modes}",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"`{name}` does not support modes, but `default_mode` was set in settings.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|cx| {
|
||||
@@ -223,8 +321,7 @@ impl AgentConnection for AcpConnection {
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
|
||||
response.available_commands,
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities.clone()),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -232,6 +329,7 @@ impl AgentConnection for AcpConnection {
|
||||
let session = AcpSession {
|
||||
thread: thread.downgrade(),
|
||||
suppress_abort_err: false,
|
||||
session_modes: modes
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
@@ -246,13 +344,13 @@ impl AgentConnection for AcpConnection {
|
||||
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
|
||||
let conn = self.connection.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = conn
|
||||
.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
})
|
||||
.await?;
|
||||
conn.authenticate(acp::AuthenticateRequest {
|
||||
method_id: method_id.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -303,6 +401,7 @@ impl AgentConnection for AcpConnection {
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
@@ -322,17 +421,85 @@ impl AgentConnection for AcpConnection {
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
meta: None,
|
||||
};
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn session_modes(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionModes>> {
|
||||
let sessions = self.sessions.clone();
|
||||
let sessions_ref = sessions.borrow();
|
||||
let Some(session) = sessions_ref.get(session_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(modes) = session.session_modes.as_ref() {
|
||||
Some(Rc::new(AcpSessionModes {
|
||||
connection: self.connection.clone(),
|
||||
session_id: session_id.clone(),
|
||||
state: modes.clone(),
|
||||
}) as _)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpSessionModes {
|
||||
session_id: acp::SessionId,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
state: Rc<RefCell<acp::SessionModeState>>,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionModes for AcpSessionModes {
|
||||
fn current_mode(&self) -> acp::SessionModeId {
|
||||
self.state.borrow().current_mode_id.clone()
|
||||
}
|
||||
|
||||
fn all_modes(&self) -> Vec<acp::SessionMode> {
|
||||
self.state.borrow().available_modes.clone()
|
||||
}
|
||||
|
||||
fn set_mode(&self, mode_id: acp::SessionModeId, cx: &mut App) -> Task<Result<()>> {
|
||||
let connection = self.connection.clone();
|
||||
let session_id = self.session_id.clone();
|
||||
let old_mode_id;
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
old_mode_id = state.current_mode_id.clone();
|
||||
state.current_mode_id = mode_id.clone();
|
||||
};
|
||||
let state = self.state.clone();
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let result = connection
|
||||
.set_session_mode(acp::SetSessionModeRequest {
|
||||
session_id,
|
||||
mode_id,
|
||||
meta: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
state.borrow_mut().current_mode_id = old_mode_id;
|
||||
}
|
||||
|
||||
result?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
@@ -343,23 +510,40 @@ impl acp::Client for ClientDelegate {
|
||||
&self,
|
||||
arguments: acp::RequestPermissionRequest,
|
||||
) -> Result<acp::RequestPermissionResponse, acp::Error> {
|
||||
let respect_always_allow_setting;
|
||||
let thread;
|
||||
{
|
||||
let sessions_ref = self.sessions.borrow();
|
||||
let session = sessions_ref
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?;
|
||||
respect_always_allow_setting = session.session_modes.is_none();
|
||||
thread = session.thread.clone();
|
||||
}
|
||||
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let task = self
|
||||
.session_thread(&arguments.session_id)?
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
|
||||
})??;
|
||||
let task = thread.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(
|
||||
arguments.tool_call,
|
||||
arguments.options,
|
||||
respect_always_allow_setting,
|
||||
cx,
|
||||
)
|
||||
})??;
|
||||
|
||||
let outcome = task.await;
|
||||
|
||||
Ok(acp::RequestPermissionResponse { outcome })
|
||||
Ok(acp::RequestPermissionResponse {
|
||||
outcome,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
&self,
|
||||
arguments: acp::WriteTextFileRequest,
|
||||
) -> Result<(), acp::Error> {
|
||||
) -> Result<acp::WriteTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.session_thread(&arguments.session_id)?
|
||||
@@ -369,7 +553,7 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
task.await?;
|
||||
|
||||
Ok(())
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn read_text_file(
|
||||
@@ -385,17 +569,34 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse { content })
|
||||
Ok(acp::ReadTextFileResponse {
|
||||
content,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
&self,
|
||||
notification: acp::SessionNotification,
|
||||
) -> Result<(), acp::Error> {
|
||||
self.session_thread(¬ification.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions
|
||||
.get(¬ification.session_id)
|
||||
.context("Failed to get session")?;
|
||||
|
||||
if let acp::SessionUpdate::CurrentModeUpdate { current_mode_id } = ¬ification.update {
|
||||
if let Some(session_modes) = &session.session_modes {
|
||||
session_modes.borrow_mut().current_mode_id = current_mode_id.clone();
|
||||
} else {
|
||||
log::error!(
|
||||
"Got a `CurrentModeUpdate` notification, but they agent didn't specify `modes` during setting setup."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -420,26 +621,49 @@ impl acp::Client for ClientDelegate {
|
||||
Ok(
|
||||
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
|
||||
terminal_id: terminal.id().clone(),
|
||||
meta: None,
|
||||
})?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
|
||||
async fn kill_terminal_command(
|
||||
&self,
|
||||
args: acp::KillTerminalCommandRequest,
|
||||
) -> Result<acp::KillTerminalCommandResponse, acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.kill_terminal(args.terminal_id, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
|
||||
async fn ext_method(
|
||||
&self,
|
||||
_name: Arc<str>,
|
||||
_params: Arc<serde_json::value::RawValue>,
|
||||
) -> Result<Arc<serde_json::value::RawValue>, acp::Error> {
|
||||
Err(acp::Error::method_not_found())
|
||||
}
|
||||
|
||||
async fn ext_notification(
|
||||
&self,
|
||||
_name: Arc<str>,
|
||||
_params: Arc<serde_json::value::RawValue>,
|
||||
) -> Result<(), acp::Error> {
|
||||
Err(acp::Error::method_not_found())
|
||||
}
|
||||
|
||||
async fn release_terminal(
|
||||
&self,
|
||||
args: acp::ReleaseTerminalRequest,
|
||||
) -> Result<acp::ReleaseTerminalResponse, acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.release_terminal(args.terminal_id, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
async fn terminal_output(
|
||||
@@ -468,7 +692,10 @@ impl acp::Client for ClientDelegate {
|
||||
})??
|
||||
.await;
|
||||
|
||||
Ok(acp::WaitForTerminalExitResponse { exit_status })
|
||||
Ok(acp::WaitForTerminalExitResponse {
|
||||
exit_status,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,243 +2,76 @@ mod acp;
|
||||
mod claude;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
mod settings;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod e2e_tests;
|
||||
|
||||
use anyhow::Context as _;
|
||||
pub use claude::*;
|
||||
use client::ProxySettings;
|
||||
use collections::HashMap;
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
use fs::RemoveOptions;
|
||||
use fs::RenameOptions;
|
||||
use futures::StreamExt as _;
|
||||
pub use gemini::*;
|
||||
use gpui::AppContext;
|
||||
use node_runtime::NodeRuntime;
|
||||
pub use settings::*;
|
||||
use http_client::read_no_proxy_from_env;
|
||||
use project::agent_server_store::AgentServerStore;
|
||||
|
||||
use acp_thread::AgentConnection;
|
||||
use acp_thread::LoadError;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AsyncApp, Entity, SharedString, Task};
|
||||
use gpui::{App, AppContext, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr as _;
|
||||
use std::{
|
||||
any::Any,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use settings::SettingsStore;
|
||||
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
settings::init(cx);
|
||||
}
|
||||
pub use acp::AcpConnection;
|
||||
|
||||
pub struct AgentServerDelegate {
|
||||
store: Entity<AgentServerStore>,
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_available: Option<watch::Sender<Option<String>>>,
|
||||
}
|
||||
|
||||
impl AgentServerDelegate {
|
||||
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
|
||||
Self { project, status_tx }
|
||||
pub fn new(
|
||||
store: Entity<AgentServerStore>,
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_tx: Option<watch::Sender<Option<String>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
project,
|
||||
status_tx,
|
||||
new_version_available: new_version_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
fn get_or_npm_install_builtin_agent(
|
||||
self,
|
||||
binary_name: SharedString,
|
||||
package_name: SharedString,
|
||||
entrypoint_path: PathBuf,
|
||||
ignore_system_version: bool,
|
||||
minimum_version: Option<Version>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<AgentServerCommand>> {
|
||||
let project = self.project;
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"External agents are not yet available in remote projects."
|
||||
)));
|
||||
};
|
||||
let status_tx = self.status_tx;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if !ignore_system_version {
|
||||
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
|
||||
return Ok(AgentServerCommand {
|
||||
path: bin,
|
||||
args: Vec::new(),
|
||||
env: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let node_path = node_runtime.binary_path().await?;
|
||||
let dir = paths::data_dir()
|
||||
.join("external_agents")
|
||||
.join(binary_name.as_str());
|
||||
fs.create_dir(&dir).await?;
|
||||
|
||||
let mut stream = fs.read_dir(&dir).await?;
|
||||
let mut versions = Vec::new();
|
||||
let mut to_delete = Vec::new();
|
||||
while let Some(entry) = stream.next().await {
|
||||
let Ok(entry) = entry else { continue };
|
||||
let Some(file_name) = entry.file_name() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(version) = file_name
|
||||
.to_str()
|
||||
.and_then(|name| semver::Version::from_str(&name).ok())
|
||||
{
|
||||
versions.push((version, file_name.to_owned()));
|
||||
} else {
|
||||
to_delete.push(file_name.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
versions.sort();
|
||||
let newest_version = if let Some((version, file_name)) = versions.last().cloned()
|
||||
&& minimum_version.is_none_or(|minimum_version| version >= minimum_version)
|
||||
{
|
||||
versions.pop();
|
||||
Some(file_name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
log::debug!("existing version of {package_name}: {newest_version:?}");
|
||||
to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
|
||||
|
||||
cx.background_spawn({
|
||||
let fs = fs.clone();
|
||||
let dir = dir.clone();
|
||||
async move {
|
||||
for file_name in to_delete {
|
||||
fs.remove_dir(
|
||||
&dir.join(file_name),
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let version = if let Some(file_name) = newest_version {
|
||||
cx.background_spawn({
|
||||
let file_name = file_name.clone();
|
||||
let dir = dir.clone();
|
||||
async move {
|
||||
let latest_version =
|
||||
node_runtime.npm_package_latest_version(&package_name).await;
|
||||
if let Ok(latest_version) = latest_version
|
||||
&& &latest_version != &file_name.to_string_lossy()
|
||||
{
|
||||
Self::download_latest_version(
|
||||
fs,
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
file_name
|
||||
} else {
|
||||
if let Some(mut status_tx) = status_tx {
|
||||
status_tx.send("Installing…".into()).ok();
|
||||
}
|
||||
let dir = dir.clone();
|
||||
cx.background_spawn(Self::download_latest_version(
|
||||
fs,
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
};
|
||||
anyhow::Ok(AgentServerCommand {
|
||||
path: node_path,
|
||||
args: vec![
|
||||
dir.join(version)
|
||||
.join(entrypoint_path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
],
|
||||
env: Default::default(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_latest_version(
|
||||
fs: Arc<dyn Fs>,
|
||||
dir: PathBuf,
|
||||
node_runtime: NodeRuntime,
|
||||
package_name: SharedString,
|
||||
) -> Result<String> {
|
||||
log::debug!("downloading latest version of {package_name}");
|
||||
|
||||
let tmp_dir = tempfile::tempdir_in(&dir)?;
|
||||
|
||||
node_runtime
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
|
||||
.await?;
|
||||
|
||||
let version = node_runtime
|
||||
.npm_package_installed_version(tmp_dir.path(), &package_name)
|
||||
.await?
|
||||
.context("expected package to be installed")?;
|
||||
|
||||
fs.rename(
|
||||
&tmp_dir.keep(),
|
||||
&dir.join(&version),
|
||||
RenameOptions {
|
||||
ignore_if_exists: true,
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> SharedString;
|
||||
fn telemetry_id(&self) -> &'static str;
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
fn set_default_mode(
|
||||
&self,
|
||||
_mode_id: Option<agent_client_protocol::SessionModeId>,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
@@ -249,119 +82,24 @@ impl dyn AgentServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AgentServerCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let filtered_env = self.env.as_ref().map(|env| {
|
||||
env.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
if util::redact::should_redact(k) {
|
||||
"[REDACTED]"
|
||||
} else {
|
||||
v
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
/// Load the default proxy environment variables to pass through to the agent
|
||||
pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
|
||||
let proxy_url = cx
|
||||
.read_global(|settings: &SettingsStore, _| settings.get::<ProxySettings>(None).proxy_url());
|
||||
let mut env = HashMap::default();
|
||||
|
||||
f.debug_struct("AgentServerCommand")
|
||||
.field("path", &self.path)
|
||||
.field("args", &self.args)
|
||||
.field("env", &filtered_env)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
|
||||
pub struct AgentServerCommand {
|
||||
#[serde(rename = "command")]
|
||||
pub path: PathBuf,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl AgentServerCommand {
|
||||
pub async fn resolve(
|
||||
path_bin_name: &'static str,
|
||||
extra_args: &[&'static str],
|
||||
fallback_path: Option<&Path>,
|
||||
settings: Option<BuiltinAgentServerSettings>,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Self> {
|
||||
if let Some(settings) = settings
|
||||
&& let Some(command) = settings.custom_command()
|
||||
{
|
||||
Some(command)
|
||||
if let Some(proxy_url) = &proxy_url {
|
||||
let env_var = if proxy_url.scheme() == "https" {
|
||||
"HTTPS_PROXY"
|
||||
} else {
|
||||
match find_bin_in_path(path_bin_name.into(), project, cx).await {
|
||||
Some(path) => Some(Self {
|
||||
path,
|
||||
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
|
||||
env: None,
|
||||
}),
|
||||
None => fallback_path.and_then(|path| {
|
||||
if path.exists() {
|
||||
Some(Self {
|
||||
path: path.to_path_buf(),
|
||||
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
|
||||
env: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
"HTTP_PROXY"
|
||||
};
|
||||
env.insert(env_var.to_owned(), proxy_url.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_bin_in_path(
|
||||
bin_name: SharedString,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<PathBuf> {
|
||||
let (env_task, root_dir) = project
|
||||
.update(cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next();
|
||||
match worktree {
|
||||
Some(worktree) => {
|
||||
let env_task = project.environment().update(cx, |env, cx| {
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
});
|
||||
|
||||
let path = worktree.read(cx).abs_path();
|
||||
(env_task, path)
|
||||
}
|
||||
None => {
|
||||
let path: Arc<Path> = paths::home_dir().as_path().into();
|
||||
let env_task = project.environment().update(cx, |env, cx| {
|
||||
env.get_directory_environment(path.clone(), cx)
|
||||
});
|
||||
(env_task, path)
|
||||
}
|
||||
}
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let which_result = if cfg!(windows) {
|
||||
which::which(bin_name.as_str())
|
||||
} else {
|
||||
let env = env_task.await.unwrap_or_default();
|
||||
let shell_path = env.get("PATH").cloned();
|
||||
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
|
||||
};
|
||||
|
||||
if let Err(which::Error::CannotFindBinaryPath) = which_result {
|
||||
return None;
|
||||
}
|
||||
|
||||
which_result.log_err()
|
||||
})
|
||||
.await
|
||||
|
||||
if let Some(no_proxy) = read_no_proxy_from_env() {
|
||||
env.insert("NO_PROXY".to_owned(), no_proxy);
|
||||
}
|
||||
|
||||
env
|
||||
}
|
||||
|
||||
@@ -1,61 +1,26 @@
|
||||
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
use agent_client_protocol as acp;
|
||||
use fs::Fs;
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClaudeCode;
|
||||
|
||||
pub struct ClaudeCodeLoginCommand {
|
||||
pub struct AgentServerLoginCommand {
|
||||
pub path: PathBuf,
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
impl ClaudeCode {
|
||||
const BINARY_NAME: &'static str = "claude-code-acp";
|
||||
const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp";
|
||||
|
||||
pub fn login_command(
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<ClaudeCodeLoginCommand>> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
delegate.get_or_npm_install_builtin_agent(
|
||||
Self::BINARY_NAME.into(),
|
||||
Self::PACKAGE_NAME.into(),
|
||||
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
|
||||
true,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
command.args.push("/login".into());
|
||||
|
||||
Ok(ClaudeCodeLoginCommand {
|
||||
path: command.path,
|
||||
arguments: command.args,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"claude-code"
|
||||
@@ -69,47 +34,60 @@ impl AgentServer for ClaudeCode {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let server_name = self.name();
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
update_settings_file::<AllAgentServersSettings>(fs, cx, |settings, _| {
|
||||
settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
delegate.get_or_npm_install_builtin_agent(
|
||||
Self::BINARY_NAME.into(),
|
||||
Self::PACKAGE_NAME.into(),
|
||||
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
|
||||
true,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
|
||||
if let Some(api_key) = cx
|
||||
.update(AnthropicLanguageModelProvider::api_key)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&CLAUDE_CODE_NAME.into())
|
||||
.context("Claude Code is not registered")?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
extra_env,
|
||||
delegate.status_tx,
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
use crate::{AgentServerCommand, AgentServerDelegate};
|
||||
use crate::{AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::Result;
|
||||
use gpui::{App, SharedString, Task};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use std::{path::Path, rc::Rc, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
/// A generic agent server implementation for custom user-defined agents
|
||||
pub struct CustomAgentServer {
|
||||
name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl CustomAgentServer {
|
||||
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
|
||||
Self { name, command }
|
||||
pub fn new(name: SharedString) -> Self {
|
||||
Self { name }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,16 +33,68 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
let name = self.name();
|
||||
update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
|
||||
settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
_delegate: AgentServerDelegate,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let server_name = self.name();
|
||||
let command = self.command.clone();
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await)
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&ExternalAgentServerName(name.clone()))
|
||||
.with_context(|| {
|
||||
format!("Custom agent server `{}` is not registered", name)
|
||||
})?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
extra_env,
|
||||
delegate.status_tx,
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
#[cfg(test)]
|
||||
use crate::{AgentServerCommand, CustomAgentServerSettings};
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{AppContext, Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
#[cfg(test)]
|
||||
use project::agent_server_store::BuiltinAgentServerSettings;
|
||||
use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -83,6 +83,7 @@ where
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Read the file ".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
uri: "foo.rs".into(),
|
||||
@@ -92,10 +93,12 @@ where
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
}),
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: " and tell me what the content of the println! is".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
],
|
||||
cx,
|
||||
@@ -449,7 +452,6 @@ pub use common_e2e_tests;
|
||||
// Helpers
|
||||
|
||||
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
#[cfg(test)]
|
||||
use settings::Settings;
|
||||
|
||||
env_logger::try_init().ok();
|
||||
@@ -468,17 +470,17 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client, cx);
|
||||
agent_settings::init(cx);
|
||||
crate::settings::init(cx);
|
||||
AllAgentServersSettings::register(cx);
|
||||
|
||||
#[cfg(test)]
|
||||
crate::AllAgentServersSettings::override_global(
|
||||
crate::AllAgentServersSettings {
|
||||
claude: Some(CustomAgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: "claude-code-acp".into(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
},
|
||||
AllAgentServersSettings::override_global(
|
||||
AllAgentServersSettings {
|
||||
claude: Some(BuiltinAgentServerSettings {
|
||||
path: Some("claude-code-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
custom: collections::HashMap::default(),
|
||||
@@ -498,10 +500,11 @@ pub async fn new_test_thread(
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None);
|
||||
let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
|
||||
let delegate = AgentServerDelegate::new(store, project.clone(), None, None);
|
||||
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
|
||||
let (connection, _) = cx
|
||||
.update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::acp::AcpConnection;
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
use project::agent_server_store::GEMINI_NAME;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
|
||||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"gemini-cli"
|
||||
@@ -31,112 +26,52 @@ impl AgentServer for Gemini {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
});
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
let mut extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let ignore_system_version = settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.ignore_system_version)
|
||||
.unwrap_or(true);
|
||||
let mut command = if let Some(settings) = settings
|
||||
&& let Some(command) = settings.custom_command()
|
||||
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
||||
|
||||
if let Some(api_key) = cx
|
||||
.update(GoogleLanguageModelProvider::api_key_for_gemini_cli)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
command
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
delegate.get_or_npm_install_builtin_agent(
|
||||
Self::BINARY_NAME.into(),
|
||||
Self::PACKAGE_NAME.into(),
|
||||
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
|
||||
ignore_system_version,
|
||||
Some(Self::MINIMUM_VERSION.parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
if !command.args.contains(&ACP_ARG.into()) {
|
||||
command.args.push(ACP_ARG.into());
|
||||
extra_env.insert("GEMINI_API_KEY".into(), api_key);
|
||||
}
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&GEMINI_NAME.into())
|
||||
.context("Gemini CLI is not registered")?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
extra_env,
|
||||
delegate.status_tx,
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
match &result {
|
||||
Ok(connection) => {
|
||||
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
|
||||
&& !connection.prompt_capabilities().image
|
||||
{
|
||||
let version_output = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output()
|
||||
.await;
|
||||
let current_version =
|
||||
String::from_utf8(version_output?.stdout)?.trim().to_owned();
|
||||
|
||||
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) =
|
||||
futures::future::join(version_fut, help_fut).await;
|
||||
let Some(version_output) = version_output.ok().and_then(|output| String::from_utf8(output.stdout).ok()) else {
|
||||
return result;
|
||||
};
|
||||
let Some((help_stdout, help_stderr)) = help_output.ok().and_then(|output| String::from_utf8(output.stdout).ok().zip(String::from_utf8(output.stderr).ok())) else {
|
||||
return result;
|
||||
};
|
||||
|
||||
let current_version = version_output.trim().to_string();
|
||||
let supported = help_stdout.contains(ACP_ARG) || current_version.parse::<semver::Version>().is_ok_and(|version| version >= Self::MINIMUM_VERSION.parse::<semver::Version>().unwrap());
|
||||
|
||||
log::error!("failed to create ACP connection to gemini (version is {current_version}, supported: {supported}): {e}");
|
||||
log::debug!("gemini --help stdout: {help_stdout:?}");
|
||||
log::debug!("gemini --help stderr: {help_stderr:?}");
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
let connection = crate::acp::connect(
|
||||
name,
|
||||
command,
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -145,18 +80,11 @@ impl AgentServer for Gemini {
|
||||
}
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
const PACKAGE_NAME: &str = "@google/gemini-cli";
|
||||
|
||||
const MINIMUM_VERSION: &str = "0.2.1";
|
||||
|
||||
const BINARY_NAME: &str = "gemini";
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
|
||||
use super::*;
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<CustomAgentServerSettings>,
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct BuiltinAgentServerSettings {
|
||||
/// Absolute path to a binary to be used when launching this agent.
|
||||
///
|
||||
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
|
||||
#[serde(rename = "command")]
|
||||
pub path: Option<PathBuf>,
|
||||
/// If a binary is specified in `command`, it will be passed these arguments.
|
||||
pub args: Option<Vec<String>>,
|
||||
/// If a binary is specified in `command`, it will be passed these environment variables.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Whether to skip searching `$PATH` for an agent server binary when
|
||||
/// launching this agent.
|
||||
///
|
||||
/// This has no effect if a `command` is specified. Otherwise, when this is
|
||||
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
|
||||
/// is found, use it for threads with this agent. If no agent binary is
|
||||
/// found on `$PATH`, Zed will automatically install and use its own binary.
|
||||
/// When this is `true`, Zed will not search `$PATH`, and will always use
|
||||
/// its own binary.
|
||||
///
|
||||
/// Default: true
|
||||
pub ignore_system_version: Option<bool>,
|
||||
}
|
||||
|
||||
impl BuiltinAgentServerSettings {
|
||||
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
|
||||
self.path.map(|path| AgentServerCommand {
|
||||
path,
|
||||
args: self.args.unwrap_or_default(),
|
||||
env: self.env,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
|
||||
fn from(value: AgentServerCommand) -> Self {
|
||||
BuiltinAgentServerSettings {
|
||||
path: Some(value.path),
|
||||
args: Some(value.args),
|
||||
env: value.env,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct CustomAgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings {
|
||||
gemini,
|
||||
claude,
|
||||
custom,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
|
||||
// Merge custom agents
|
||||
for (name, config) in custom {
|
||||
// Skip built-in agent names to avoid conflicts
|
||||
if name != "gemini" && name != "claude" {
|
||||
settings.custom.insert(name.clone(), config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -15,11 +15,14 @@ path = "src/agent_settings.rs"
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
convert_case.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::IndexMap;
|
||||
use gpui::SharedString;
|
||||
use convert_case::{Case, Casing as _};
|
||||
use fs::Fs;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::AgentSettings;
|
||||
|
||||
pub mod builtin_profiles {
|
||||
use super::AgentProfileId;
|
||||
@@ -38,6 +44,69 @@ impl Default for AgentProfileId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AgentProfile {
|
||||
id: AgentProfileId,
|
||||
}
|
||||
|
||||
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
|
||||
|
||||
impl AgentProfile {
|
||||
pub fn new(id: AgentProfileId) -> Self {
|
||||
Self { id }
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &AgentProfileId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
/// Saves a new profile to the settings.
|
||||
pub fn create(
|
||||
name: String,
|
||||
base_profile_id: Option<AgentProfileId>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) -> AgentProfileId {
|
||||
let id = AgentProfileId(name.to_case(Case::Kebab).into());
|
||||
|
||||
let base_profile =
|
||||
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
|
||||
|
||||
let profile_settings = AgentProfileSettings {
|
||||
name: name.into(),
|
||||
tools: base_profile
|
||||
.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(),
|
||||
};
|
||||
|
||||
update_settings_file::<AgentSettings>(fs, cx, {
|
||||
let id = id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.create_profile(id, profile_settings).log_err();
|
||||
}
|
||||
});
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Returns a map of AgentProfileIds to their names
|
||||
pub fn available_profiles(cx: &App) -> AvailableProfiles {
|
||||
let mut profiles = AvailableProfiles::default();
|
||||
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
|
||||
profiles.insert(id.clone(), profile.name.clone());
|
||||
}
|
||||
profiles
|
||||
}
|
||||
}
|
||||
|
||||
/// A profile for the Zed Agent that controls its behavior.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgentProfileSettings {
|
||||
|
||||
@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
@@ -75,6 +75,7 @@ pub struct AgentSettings {
|
||||
pub expand_edit_card: bool,
|
||||
pub expand_terminal_card: bool,
|
||||
pub use_modifier_to_send: bool,
|
||||
pub message_editor_min_lines: usize,
|
||||
}
|
||||
|
||||
impl AgentSettings {
|
||||
@@ -107,6 +108,10 @@ impl AgentSettings {
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_message_editor_max_lines(&self) -> usize {
|
||||
self.message_editor_min_lines * 2
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
@@ -223,7 +228,8 @@ impl AgentSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent", fallback_key = "assistant")]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -268,6 +274,10 @@ pub struct AgentSettingsContent {
|
||||
/// Whenever a tool action would normally wait for your confirmation
|
||||
/// that you allow it, always choose to allow it.
|
||||
///
|
||||
/// This setting has no effect on external agents that support permission modes, such as Claude Code.
|
||||
///
|
||||
/// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
|
||||
///
|
||||
/// Default: false
|
||||
always_allow_tool_actions: Option<bool>,
|
||||
/// Where to show a popup notification when the agent is waiting for user input.
|
||||
@@ -315,6 +325,10 @@ pub struct AgentSettingsContent {
|
||||
///
|
||||
/// Default: false
|
||||
use_modifier_to_send: Option<bool>,
|
||||
/// Minimum number of lines of height the agent message editor should have.
|
||||
///
|
||||
/// Default: 4
|
||||
message_editor_min_lines: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
@@ -350,21 +364,30 @@ impl JsonSchema for LanguageModelProviderSetting {
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
// list the builtin providers as a subset so that we still auto complete them in the settings
|
||||
json_schema!({
|
||||
"enum": [
|
||||
"amazon-bedrock",
|
||||
"anthropic",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"mistral",
|
||||
"ollama",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"vercel",
|
||||
"x_ai",
|
||||
"zed.dev"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"amazon-bedrock",
|
||||
"anthropic",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"mistral",
|
||||
"ollama",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"vercel",
|
||||
"x_ai",
|
||||
"zed.dev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -399,10 +422,6 @@ pub struct ContextServerPresetContent {
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
const KEY: Option<&'static str> = Some("agent");
|
||||
|
||||
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AgentSettingsContent;
|
||||
@@ -471,6 +490,10 @@ impl Settings for AgentSettings {
|
||||
&mut settings.use_modifier_to_send,
|
||||
value.use_modifier_to_send,
|
||||
);
|
||||
merge(
|
||||
&mut settings.message_editor_min_lines,
|
||||
value.message_editor_min_lines,
|
||||
);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
|
||||
@@ -25,6 +25,7 @@ agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
assistant_context.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_slash_commands.workspace = true
|
||||
@@ -51,7 +52,6 @@ gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
@@ -80,7 +80,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
settings.workspace = true
|
||||
shlex.workspace = true
|
||||
smol.workspace = true
|
||||
streaming_diff.workspace = true
|
||||
task.workspace = true
|
||||
@@ -97,7 +96,6 @@ ui_input.workspace = true
|
||||
url.workspace = true
|
||||
urlencoding.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
mod completion_provider;
|
||||
mod entry_view_state;
|
||||
mod message_editor;
|
||||
mod mode_selector;
|
||||
mod model_selector;
|
||||
mod model_selector_popover;
|
||||
mod thread_history;
|
||||
mod thread_view;
|
||||
|
||||
pub use mode_selector::ModeSelector;
|
||||
pub use model_selector::AcpModelSelector;
|
||||
pub use model_selector_popover::AcpModelSelectorPopover;
|
||||
pub use thread_history::*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -15,7 +15,8 @@ use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::{
|
||||
Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
|
||||
ProjectPath, Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
@@ -67,7 +68,7 @@ pub struct ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ impl ContextPickerCompletionProvider {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -599,7 +600,7 @@ impl ContextPickerCompletionProvider {
|
||||
}),
|
||||
);
|
||||
|
||||
if self.prompt_capabilities.get().embedded_context {
|
||||
if self.prompt_capabilities.borrow().embedded_context {
|
||||
const RECENT_COUNT: usize = 2;
|
||||
let threads = self
|
||||
.history_store
|
||||
@@ -621,7 +622,7 @@ impl ContextPickerCompletionProvider {
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut App,
|
||||
) -> Vec<ContextPickerEntry> {
|
||||
let embedded_context = self.prompt_capabilities.get().embedded_context;
|
||||
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
|
||||
let mut entries = if embedded_context {
|
||||
vec![
|
||||
ContextPickerEntry::Mode(ContextPickerMode::File),
|
||||
@@ -693,7 +694,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
ContextCompletion::try_parse(
|
||||
line,
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
self.prompt_capabilities.borrow().embedded_context,
|
||||
)
|
||||
});
|
||||
let Some(state) = state else {
|
||||
@@ -732,7 +733,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(command.name.to_string(), None),
|
||||
documentation: Some(CompletionDocumentation::SingleLine(
|
||||
documentation: Some(CompletionDocumentation::MultiLinePlainText(
|
||||
command.description.into(),
|
||||
)),
|
||||
source: project::CompletionSource::Custom,
|
||||
@@ -771,6 +772,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions {
|
||||
dynamic_width: true,
|
||||
},
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
@@ -862,6 +866,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions {
|
||||
dynamic_width: true,
|
||||
},
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
@@ -889,7 +896,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
ContextCompletion::try_parse(
|
||||
line,
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
self.prompt_capabilities.borrow().embedded_context,
|
||||
)
|
||||
.map(|completion| {
|
||||
completion.source_range().start <= offset_to_line + position.column as usize
|
||||
@@ -1005,56 +1012,44 @@ impl ContextCompletion {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct SlashCommandCompletion {
|
||||
source_range: Range<usize>,
|
||||
command: Option<String>,
|
||||
argument: Option<String>,
|
||||
pub struct SlashCommandCompletion {
|
||||
pub source_range: Range<usize>,
|
||||
pub command: Option<String>,
|
||||
pub argument: Option<String>,
|
||||
}
|
||||
|
||||
impl SlashCommandCompletion {
|
||||
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
// If we decide to support commands that are not at the beginning of the prompt, we can remove this check
|
||||
if !line.starts_with('/') || offset_to_line != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let last_command_start = line.rfind('/')?;
|
||||
if last_command_start >= line.len() {
|
||||
return Some(Self::default());
|
||||
}
|
||||
if last_command_start > 0
|
||||
&& line
|
||||
.chars()
|
||||
.nth(last_command_start - 1)
|
||||
.is_some_and(|c| !c.is_whitespace())
|
||||
let (prefix, last_command) = line.rsplit_once('/')?;
|
||||
if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
|
||||
|| last_command.starts_with(char::is_whitespace)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let rest_of_line = &line[last_command_start + 1..];
|
||||
|
||||
let mut command = None;
|
||||
let mut argument = None;
|
||||
let mut end = last_command_start + 1;
|
||||
|
||||
if let Some(command_text) = rest_of_line.split_whitespace().next() {
|
||||
command = Some(command_text.to_string());
|
||||
end += command_text.len();
|
||||
|
||||
// Find the start of arguments after the command
|
||||
if let Some(args_start) =
|
||||
rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
|
||||
{
|
||||
let args = &rest_of_line[command_text.len() + args_start..].trim_end();
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.to_string());
|
||||
end += args.len() + 1;
|
||||
}
|
||||
let mut command = None;
|
||||
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.trim_end().to_string());
|
||||
}
|
||||
}
|
||||
command = Some(command_text.to_string());
|
||||
} else if !last_command.is_empty() {
|
||||
command = Some(last_command.to_string());
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
source_range: last_command_start + offset_to_line..end + offset_to_line,
|
||||
source_range: prefix.len() + offset_to_line
|
||||
..line
|
||||
.rfind(|c: char| !c.is_whitespace())
|
||||
.unwrap_or_else(|| line.len())
|
||||
+ 1
|
||||
+ offset_to_line,
|
||||
command,
|
||||
argument,
|
||||
})
|
||||
@@ -1071,13 +1066,21 @@ struct MentionCompletion {
|
||||
impl MentionCompletion {
|
||||
fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
|
||||
let last_mention_start = line.rfind('@')?;
|
||||
if last_mention_start >= line.len() {
|
||||
return Some(Self::default());
|
||||
|
||||
// No whitespace immediately after '@'
|
||||
if line[last_mention_start + 1..]
|
||||
.chars()
|
||||
.next()
|
||||
.is_some_and(|c| c.is_whitespace())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// Must be a word boundary before '@'
|
||||
if last_mention_start > 0
|
||||
&& line
|
||||
&& line[..last_mention_start]
|
||||
.chars()
|
||||
.nth(last_mention_start - 1)
|
||||
.last()
|
||||
.is_some_and(|c| !c.is_whitespace())
|
||||
{
|
||||
return None;
|
||||
@@ -1090,7 +1093,9 @@ impl MentionCompletion {
|
||||
|
||||
let mut parts = rest_of_line.split_whitespace();
|
||||
let mut end = last_mention_start + 1;
|
||||
|
||||
if let Some(mode_text) = parts.next() {
|
||||
// Safe since we check no leading whitespace above
|
||||
end += mode_text.len();
|
||||
|
||||
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
|
||||
@@ -1103,6 +1108,12 @@ impl MentionCompletion {
|
||||
match rest_of_line[mode_text.len()..].find(|c: char| !c.is_whitespace()) {
|
||||
Some(whitespace_count) => {
|
||||
if let Some(argument_text) = parts.next() {
|
||||
// If mode wasn't recognized but we have an argument, don't suggest completions
|
||||
// (e.g. '@something word')
|
||||
if mode.is_none() && !argument_text.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
argument = Some(argument_text.to_string());
|
||||
end += whitespace_count + argument_text.len();
|
||||
}
|
||||
@@ -1173,6 +1184,15 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..30,
|
||||
command: Some("拿不到命令".to_string()),
|
||||
argument: Some("拿不到命令".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
|
||||
@@ -1180,6 +1200,8 @@ mod tests {
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1249,6 +1271,17 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(true, "Lorem @main ", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..12,
|
||||
mode: None,
|
||||
argument: Some("main".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse(true, "Lorem @main m", 0), None);
|
||||
|
||||
assert_eq!(MentionCompletion::try_parse(true, "test@", 0), None);
|
||||
|
||||
// Allowed non-file mentions
|
||||
@@ -1263,14 +1296,27 @@ mod tests {
|
||||
);
|
||||
|
||||
// Disallowed non-file mentions
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(false, "Lorem @symbol main", 0),
|
||||
Some(MentionCompletion {
|
||||
source_range: 6..18,
|
||||
mode: None,
|
||||
argument: Some("main".to_string()),
|
||||
})
|
||||
None
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(true, "Lorem@symbol", 0),
|
||||
None,
|
||||
"Should not parse mention inside word"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(true, "Lorem @ file", 0),
|
||||
None,
|
||||
"Should not parse with a space after @"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
MentionCompletion::try_parse(true, "@ file", 0),
|
||||
None,
|
||||
"Should not parse with a space after @ at the start of the line"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc};
|
||||
|
||||
use acp_thread::{AcpThread, AgentThreadEntry};
|
||||
use agent_client_protocol::{self as acp, ToolCallId};
|
||||
@@ -11,7 +7,7 @@ use collections::HashMap;
|
||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||
use gpui::{
|
||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
|
||||
ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use project::Project;
|
||||
@@ -30,8 +26,9 @@ pub struct EntryViewState {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
entries: Vec<Entry>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
}
|
||||
|
||||
impl EntryViewState {
|
||||
@@ -40,8 +37,9 @@ impl EntryViewState {
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
@@ -51,6 +49,7 @@ impl EntryViewState {
|
||||
entries: Vec::new(),
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
agent_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +89,7 @@ impl EntryViewState {
|
||||
self.prompt_store.clone(),
|
||||
self.prompt_capabilities.clone(),
|
||||
self.available_commands.clone(),
|
||||
self.agent_name.clone(),
|
||||
"Edit message - @ to include context",
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
@@ -203,7 +203,7 @@ impl EntryViewState {
|
||||
self.entries.drain(range);
|
||||
}
|
||||
|
||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
@@ -444,11 +444,13 @@ mod tests {
|
||||
path: "/project/hello.txt".into(),
|
||||
old_text: Some("hi world".into()),
|
||||
new_text: "hello world".into(),
|
||||
meta: None,
|
||||
},
|
||||
}],
|
||||
locations: vec![],
|
||||
raw_input: None,
|
||||
raw_output: None,
|
||||
meta: None,
|
||||
};
|
||||
let connection = Rc::new(StubAgentConnection::new());
|
||||
let thread = cx
|
||||
@@ -476,6 +478,7 @@ mod tests {
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
acp::completion_provider::ContextPickerCompletionProvider,
|
||||
acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
|
||||
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
|
||||
};
|
||||
use acp_thread::{MentionUri, selection_name};
|
||||
@@ -8,13 +8,14 @@ use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent2::HistoryStore;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_slash_commands::codeblock_fence_for_path;
|
||||
use assistant_tool::outline;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
|
||||
ToOffset,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
|
||||
MultiBuffer, ToOffset,
|
||||
actions::Paste,
|
||||
display_map::{Crease, CreaseId, FoldId},
|
||||
display_map::{Crease, CreaseId, FoldId, Inlay},
|
||||
};
|
||||
use futures::{
|
||||
FutureExt as _,
|
||||
@@ -22,18 +23,20 @@ use futures::{
|
||||
};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, Subscription, Task,
|
||||
TextStyle, WeakEntity, pulsating_between,
|
||||
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
|
||||
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language::{Buffer, Language, language_settings::InlayHintKind};
|
||||
use language_model::LanguageModelImage;
|
||||
use postage::stream::Stream as _;
|
||||
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
|
||||
use project::{
|
||||
CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
|
||||
};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
cell::RefCell,
|
||||
ffi::OsStr,
|
||||
fmt::Write,
|
||||
ops::{Range, RangeInclusive},
|
||||
@@ -47,8 +50,8 @@ use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
|
||||
FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
|
||||
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled,
|
||||
TextSize, TintColor, Toggleable, Window, div, h_flex,
|
||||
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, Styled, TextSize, TintColor,
|
||||
Toggleable, Window, div, h_flex,
|
||||
};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
@@ -61,7 +64,9 @@ pub struct MessageEditor {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_parse_slash_command_task: Task<()>,
|
||||
}
|
||||
@@ -76,15 +81,18 @@ pub enum MessageEditorEvent {
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
const COMMAND_HINT_INLAY_ID: usize = 0;
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
placeholder: impl Into<Arc<str>>,
|
||||
agent_name: SharedString,
|
||||
placeholder: &str,
|
||||
mode: EditorMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -102,7 +110,7 @@ impl MessageEditor {
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
available_commands,
|
||||
available_commands.clone(),
|
||||
));
|
||||
let mention_set = MentionSet::default();
|
||||
let editor = cx.new(|cx| {
|
||||
@@ -110,7 +118,7 @@ impl MessageEditor {
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let mut editor = Editor::new(mode, buffer, None, window, cx);
|
||||
editor.set_placeholder_text(placeholder, cx);
|
||||
editor.set_placeholder_text(placeholder, window, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_use_modal_editing(true);
|
||||
@@ -133,12 +141,33 @@ impl MessageEditor {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut has_hint = false;
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
move |this, editor, event, window, cx| {
|
||||
if let EditorEvent::Edited { .. } = event {
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let snapshot = editor.update(cx, |editor, cx| {
|
||||
let new_hints = this
|
||||
.command_hint(editor.buffer(), cx)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let has_new_hint = !new_hints.is_empty();
|
||||
editor.splice_inlays(
|
||||
if has_hint {
|
||||
&[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
new_hints,
|
||||
cx,
|
||||
);
|
||||
has_hint = has_new_hint;
|
||||
|
||||
editor.snapshot(window, cx)
|
||||
});
|
||||
this.mention_set.remove_invalid(snapshot);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -152,11 +181,56 @@ impl MessageEditor {
|
||||
history_store,
|
||||
prompt_store,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
agent_name,
|
||||
_subscriptions: subscriptions,
|
||||
_parse_slash_command_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
|
||||
let available_commands = self.available_commands.borrow();
|
||||
if available_commands.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
|
||||
if parsed_command.argument.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let command_name = parsed_command.command?;
|
||||
let available_command = available_commands
|
||||
.iter()
|
||||
.find(|command| command.name == command_name)?;
|
||||
|
||||
let acp::AvailableCommandInput::Unstructured { mut hint } =
|
||||
available_command.input.clone()?;
|
||||
|
||||
let mut hint_pos = parsed_command.source_range.end + 1;
|
||||
if hint_pos > snapshot.len() {
|
||||
hint_pos = snapshot.len();
|
||||
hint.insert(0, ' ');
|
||||
}
|
||||
|
||||
let hint_pos = snapshot.anchor_after(hint_pos);
|
||||
|
||||
Some(Inlay::hint(
|
||||
COMMAND_HINT_INLAY_ID,
|
||||
hint_pos,
|
||||
&InlayHint {
|
||||
position: hint_pos.text_anchor,
|
||||
label: InlayHintLabel::String(hint),
|
||||
kind: Some(InlayHintKind::Parameter),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
resolve_state: project::ResolveState::Resolved,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn insert_thread_summary(
|
||||
&mut self,
|
||||
thread: agent2::DbThreadMetadata,
|
||||
@@ -354,7 +428,7 @@ impl MessageEditor {
|
||||
.unwrap_or_default();
|
||||
|
||||
if Img::extensions().contains(&extension) && !extension.contains("svg") {
|
||||
if !self.prompt_capabilities.get().image {
|
||||
if !self.prompt_capabilities.borrow().image {
|
||||
return Task::ready(Err(anyhow!("This model does not support images yet")));
|
||||
}
|
||||
let task = self
|
||||
@@ -383,11 +457,14 @@ impl MessageEditor {
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
cx.spawn(async move |_, cx| {
|
||||
let buffer = buffer.await?;
|
||||
let mention = buffer.update(cx, |buffer, cx| Mention::Text {
|
||||
content: buffer.text(),
|
||||
tracked_buffers: vec![cx.entity()],
|
||||
})?;
|
||||
anyhow::Ok(mention)
|
||||
let buffer_content =
|
||||
outline::get_buffer_content_or_outline(buffer.clone(), Some(&abs_path), &cx)
|
||||
.await?;
|
||||
|
||||
Ok(Mention::Text {
|
||||
content: buffer_content.text,
|
||||
tracked_buffers: vec![buffer],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -420,14 +497,13 @@ impl MessageEditor {
|
||||
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
|
||||
return Task::ready(Err(anyhow!("project entry not found")));
|
||||
};
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
|
||||
let directory_path = entry.path.clone();
|
||||
let worktree_id = project_path.worktree_id;
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("worktree not found")));
|
||||
};
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |_, cx| {
|
||||
let directory_path = entry.path.clone();
|
||||
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
|
||||
let file_paths = worktree.read_with(cx, |worktree, _cx| {
|
||||
collect_files_in_path(worktree, &directory_path)
|
||||
})?;
|
||||
@@ -448,18 +524,17 @@ impl MessageEditor {
|
||||
})
|
||||
});
|
||||
|
||||
// TODO: report load errors instead of just logging
|
||||
let rope_task = cx.spawn(async move |cx| {
|
||||
cx.spawn(async move |cx| {
|
||||
let buffer = open_task.await.log_err()?;
|
||||
let rope = buffer
|
||||
.read_with(cx, |buffer, _cx| buffer.as_rope().clone())
|
||||
.log_err()?;
|
||||
Some((rope, buffer))
|
||||
});
|
||||
let buffer_content = outline::get_buffer_content_or_outline(
|
||||
buffer.clone(),
|
||||
Some(&full_path),
|
||||
&cx,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let (rope, buffer) = rope_task.await?;
|
||||
Some((rel_path, full_path, rope.to_string(), buffer))
|
||||
Some((rel_path, full_path, buffer_content.text, buffer))
|
||||
})
|
||||
}))
|
||||
})?;
|
||||
@@ -627,10 +702,15 @@ impl MessageEditor {
|
||||
self.project.read(cx).fs().clone(),
|
||||
self.history_store.clone(),
|
||||
));
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None);
|
||||
let connection = server.connect(Path::new(""), delegate, cx);
|
||||
let delegate = AgentServerDelegate::new(
|
||||
self.project.read(cx).agent_server_store().clone(),
|
||||
self.project.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let connection = server.connect(None, delegate, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let agent = connection.await?;
|
||||
let (agent, _) = connection.await?;
|
||||
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
|
||||
let summary = agent
|
||||
.0
|
||||
@@ -661,20 +741,62 @@ impl MessageEditor {
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_slash_commands(
|
||||
text: &str,
|
||||
available_commands: &[acp::AvailableCommand],
|
||||
agent_name: &str,
|
||||
) -> Result<()> {
|
||||
if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
|
||||
if let Some(command_name) = parsed_command.command {
|
||||
// Check if this command is in the list of available commands from the server
|
||||
let is_supported = available_commands
|
||||
.iter()
|
||||
.any(|cmd| cmd.name == command_name);
|
||||
|
||||
if !is_supported {
|
||||
return Err(anyhow!(
|
||||
"The /{} command is not supported by {}.\n\nAvailable commands: {}",
|
||||
command_name,
|
||||
agent_name,
|
||||
if available_commands.is_empty() {
|
||||
"none".to_string()
|
||||
} else {
|
||||
available_commands
|
||||
.iter()
|
||||
.map(|cmd| format!("/{}", cmd.name))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn contents(
|
||||
&self,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
|
||||
// Check for unsupported slash commands before spawning async task
|
||||
let text = self.editor.read(cx).text(cx);
|
||||
let available_commands = self.available_commands.borrow().clone();
|
||||
if let Err(err) =
|
||||
Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
|
||||
{
|
||||
return Task::ready(Err(err));
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.mention_set
|
||||
.contents(&self.prompt_capabilities.get(), cx);
|
||||
.contents(&self.prompt_capabilities.borrow(), cx);
|
||||
let editor = self.editor.clone();
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let contents = contents.await?;
|
||||
let mut all_tracked_buffers = Vec::new();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let result = editor.update(cx, |editor, cx| {
|
||||
let mut ix = 0;
|
||||
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
|
||||
let text = editor.text(cx);
|
||||
@@ -712,8 +834,10 @@ impl MessageEditor {
|
||||
mime_type: None,
|
||||
text: content.clone(),
|
||||
uri: uri.to_uri().to_string(),
|
||||
meta: None,
|
||||
},
|
||||
),
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
Mention::Image(mention_image) => {
|
||||
@@ -733,6 +857,7 @@ impl MessageEditor {
|
||||
data: mention_image.data.to_string(),
|
||||
mime_type: mention_image.format.mime_type().into(),
|
||||
uri,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
Mention::UriOnly => {
|
||||
@@ -744,6 +869,7 @@ impl MessageEditor {
|
||||
mime_type: None,
|
||||
size: None,
|
||||
title: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
@@ -767,9 +893,9 @@ impl MessageEditor {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(chunks, all_tracked_buffers)
|
||||
})
|
||||
Ok((chunks, all_tracked_buffers))
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -798,7 +924,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.prompt_capabilities.get().image {
|
||||
if !self.prompt_capabilities.borrow().image {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -973,11 +1099,16 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let buffer = self.editor.read(cx).buffer().clone();
|
||||
let Some(buffer) = buffer.read(cx).as_singleton() else {
|
||||
let editor = self.editor.read(cx);
|
||||
let editor_buffer = editor.buffer().read(cx);
|
||||
let Some(buffer) = editor_buffer.as_singleton() else {
|
||||
return;
|
||||
};
|
||||
let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
|
||||
let cursor_anchor = editor.selections.newest_anchor().head();
|
||||
let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx));
|
||||
let anchor = buffer.update(cx, |buffer, _cx| {
|
||||
buffer.anchor_before(cursor_offset.min(buffer.len()))
|
||||
});
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
@@ -991,13 +1122,7 @@ impl MessageEditor {
|
||||
return;
|
||||
};
|
||||
self.editor.update(cx, |message_editor, cx| {
|
||||
message_editor.edit(
|
||||
[(
|
||||
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
|
||||
completion.new_text,
|
||||
)],
|
||||
cx,
|
||||
);
|
||||
message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx);
|
||||
});
|
||||
if let Some(confirm) = completion.confirm {
|
||||
confirm(CompletionIntent::Complete, window, cx);
|
||||
@@ -1066,6 +1191,7 @@ impl MessageEditor {
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
meta: _,
|
||||
}) => {
|
||||
let mention_uri = if let Some(uri) = uri {
|
||||
MentionUri::parse(&uri)
|
||||
@@ -1184,6 +1310,7 @@ impl Render for MessageEditor {
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
inlay_hints_style: editor::make_inlay_hints_style(cx),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -1448,18 +1575,13 @@ impl Addon for MessageEditorAddon {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
ops::Range,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::MentionUri;
|
||||
use agent_client_protocol as acp;
|
||||
use agent2::HistoryStore;
|
||||
use assistant_context::ContextStore;
|
||||
use assistant_tool::outline;
|
||||
use editor::{AnchorRangeExt as _, Editor, EditorMode};
|
||||
use fs::FakeFs;
|
||||
use futures::StreamExt as _;
|
||||
@@ -1502,6 +1624,7 @@ mod tests {
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
@@ -1579,6 +1702,141 @@ mod tests {
|
||||
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
".zed": {
|
||||
"tasks.json": r#"[{"label": "test", "command": "echo"}]"#
|
||||
},
|
||||
"src": {
|
||||
"main.rs": "fn main() {}",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
|
||||
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
// Start with no available commands - simulating Claude which doesn't support slash commands
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let workspace_handle = workspace.downgrade();
|
||||
let message_editor = workspace.update_in(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle.clone(),
|
||||
project.clone(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
"Claude Code".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
|
||||
|
||||
// Test that slash commands fail when no available_commands are set (empty list means no commands supported)
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("/file test.txt", window, cx);
|
||||
});
|
||||
|
||||
let contents_result = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await;
|
||||
|
||||
// Should fail because available_commands is empty (no commands supported)
|
||||
assert!(contents_result.is_err());
|
||||
let error_message = contents_result.unwrap_err().to_string();
|
||||
assert!(error_message.contains("not supported by Claude Code"));
|
||||
assert!(error_message.contains("Available commands: none"));
|
||||
|
||||
// Now simulate Claude providing its list of available commands (which doesn't include file)
|
||||
available_commands.replace(vec![acp::AvailableCommand {
|
||||
name: "help".to_string(),
|
||||
description: "Get help".to_string(),
|
||||
input: None,
|
||||
meta: None,
|
||||
}]);
|
||||
|
||||
// Test that unsupported slash commands trigger an error when we have a list of available commands
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("/file test.txt", window, cx);
|
||||
});
|
||||
|
||||
let contents_result = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await;
|
||||
|
||||
assert!(contents_result.is_err());
|
||||
let error_message = contents_result.unwrap_err().to_string();
|
||||
assert!(error_message.contains("not supported by Claude Code"));
|
||||
assert!(error_message.contains("/file"));
|
||||
assert!(error_message.contains("Available commands: /help"));
|
||||
|
||||
// Test that supported commands work fine
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("/help", window, cx);
|
||||
});
|
||||
|
||||
let contents_result = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await;
|
||||
|
||||
// Should succeed because /help is in available_commands
|
||||
assert!(contents_result.is_ok());
|
||||
|
||||
// Test that regular text works fine
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Hello Claude!", window, cx);
|
||||
});
|
||||
|
||||
let (content, _) = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content.len(), 1);
|
||||
if let acp::ContentBlock::Text(text) = &content[0] {
|
||||
assert_eq!(text.text, "Hello Claude!");
|
||||
} else {
|
||||
panic!("Expected ContentBlock::Text");
|
||||
}
|
||||
|
||||
// Test that @ mentions still work
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Check this @", window, cx);
|
||||
});
|
||||
|
||||
// The @ mention functionality should not be affected
|
||||
let (content, _) = message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(content.len(), 1);
|
||||
if let acp::ContentBlock::Text(text) = &content[0] {
|
||||
assert_eq!(text.text, "Check this @");
|
||||
} else {
|
||||
panic!("Expected ContentBlock::Text");
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageEditorItem(Entity<MessageEditor>);
|
||||
|
||||
impl Item for MessageEditorItem {
|
||||
@@ -1628,19 +1886,21 @@ mod tests {
|
||||
|
||||
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
|
||||
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
|
||||
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
let available_commands = Rc::new(RefCell::new(vec![
|
||||
acp::AvailableCommand {
|
||||
name: "quick-math".to_string(),
|
||||
description: "2 + 2 = 4 - 1 = 3".to_string(),
|
||||
input: None,
|
||||
meta: None,
|
||||
},
|
||||
acp::AvailableCommand {
|
||||
name: "say-hello".to_string(),
|
||||
description: "Say hello to whoever you want".to_string(),
|
||||
input: Some(acp::AvailableCommandInput::Unstructured {
|
||||
hint: "Who do you want to say hello to?".to_string(),
|
||||
hint: "<name>".to_string(),
|
||||
}),
|
||||
meta: None,
|
||||
},
|
||||
]));
|
||||
|
||||
@@ -1654,6 +1914,7 @@ mod tests {
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
@@ -1714,7 +1975,7 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/quick-math ");
|
||||
assert_eq!(editor.display_text(cx), "/quick-math ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
@@ -1722,7 +1983,7 @@ mod tests {
|
||||
cx.simulate_input("/say");
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say");
|
||||
assert_eq!(editor.display_text(cx), "/say");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
@@ -1740,6 +2001,7 @@ mod tests {
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
@@ -1757,8 +2019,35 @@ mod tests {
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello GPT5");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello GPT5");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
|
||||
// Delete argument
|
||||
for _ in 0..4 {
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
// Hint is visible because argument was deleted
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
|
||||
// Delete last command letter
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
// Hint goes away once command no longer matches an available one
|
||||
assert_eq!(editor.text(cx), "/say-hell");
|
||||
assert_eq!(editor.display_text(cx), "/say-hell");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
});
|
||||
}
|
||||
@@ -1846,7 +2135,7 @@ mod tests {
|
||||
|
||||
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
|
||||
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
|
||||
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
|
||||
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
|
||||
|
||||
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.weak_entity();
|
||||
@@ -1858,6 +2147,7 @@ mod tests {
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
@@ -1900,10 +2190,11 @@ mod tests {
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
|
||||
prompt_capabilities.set(acp::PromptCapabilities {
|
||||
prompt_capabilities.replace(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
});
|
||||
|
||||
cx.simulate_input("Lorem ");
|
||||
@@ -1975,6 +2266,7 @@ mod tests {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let contents = message_editor
|
||||
@@ -2299,4 +2591,110 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
// Create a large file that exceeds AUTO_OUTLINE_SIZE
|
||||
const LINE: &str = "fn example_function() { /* some code */ }\n";
|
||||
let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
|
||||
assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
|
||||
|
||||
// Create a small file that doesn't exceed AUTO_OUTLINE_SIZE
|
||||
let small_content = "fn small_function() { /* small */ }\n";
|
||||
assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE);
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"large_file.rs": large_content.clone(),
|
||||
"small_file.rs": small_content,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
|
||||
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
|
||||
|
||||
let message_editor = cx.update(|window, cx| {
|
||||
cx.new(|cx| {
|
||||
let editor = MessageEditor::new(
|
||||
workspace.downgrade(),
|
||||
project.clone(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
// Enable embedded context so files are actually included
|
||||
editor.prompt_capabilities.replace(acp::PromptCapabilities {
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
..Default::default()
|
||||
});
|
||||
editor
|
||||
})
|
||||
});
|
||||
|
||||
// Test large file mention
|
||||
// Get the absolute path using the project's worktree
|
||||
let large_file_abs_path = project.read_with(cx, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let worktree_root = worktree.read(cx).abs_path();
|
||||
worktree_root.join("large_file.rs")
|
||||
});
|
||||
let large_file_task = message_editor.update(cx, |editor, cx| {
|
||||
editor.confirm_mention_for_file(large_file_abs_path, cx)
|
||||
});
|
||||
|
||||
let large_file_mention = large_file_task.await.unwrap();
|
||||
match large_file_mention {
|
||||
Mention::Text { content, .. } => {
|
||||
// Should contain outline header for large files
|
||||
assert!(content.contains("File outline for"));
|
||||
assert!(content.contains("file too large to show full content"));
|
||||
// Should not contain the full repeated content
|
||||
assert!(!content.contains(&LINE.repeat(100)));
|
||||
}
|
||||
_ => panic!("Expected Text mention for large file"),
|
||||
}
|
||||
|
||||
// Test small file mention
|
||||
// Get the absolute path using the project's worktree
|
||||
let small_file_abs_path = project.read_with(cx, |project, cx| {
|
||||
let worktree = project.worktrees(cx).next().unwrap();
|
||||
let worktree_root = worktree.read(cx).abs_path();
|
||||
worktree_root.join("small_file.rs")
|
||||
});
|
||||
let small_file_task = message_editor.update(cx, |editor, cx| {
|
||||
editor.confirm_mention_for_file(small_file_abs_path, cx)
|
||||
});
|
||||
|
||||
let small_file_mention = small_file_task.await.unwrap();
|
||||
match small_file_mention {
|
||||
Mention::Text { content, .. } => {
|
||||
// Should contain the actual content
|
||||
assert_eq!(content, small_content);
|
||||
// Should not contain outline header
|
||||
assert!(!content.contains("File outline for"));
|
||||
}
|
||||
_ => panic!("Expected Text mention for small file"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
236
crates/agent_ui/src/acp/mode_selector.rs
Normal file
236
crates/agent_ui/src/acp/mode_selector.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use acp_thread::AgentSessionModes;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::AgentServer;
|
||||
use fs::Fs;
|
||||
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use ui::{
|
||||
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{CycleModeSelector, ToggleProfileSelector};
|
||||
|
||||
pub struct ModeSelector {
|
||||
connection: Rc<dyn AgentSessionModes>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
focus_handle: FocusHandle,
|
||||
fs: Arc<dyn Fs>,
|
||||
setting_mode: bool,
|
||||
}
|
||||
|
||||
impl ModeSelector {
|
||||
pub fn new(
|
||||
session_modes: Rc<dyn AgentSessionModes>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
focus_handle: FocusHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
connection: session_modes,
|
||||
agent_server,
|
||||
menu_handle: PopoverMenuHandle::default(),
|
||||
fs,
|
||||
setting_mode: false,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
|
||||
self.menu_handle.clone()
|
||||
}
|
||||
|
||||
pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let all_modes = self.connection.all_modes();
|
||||
let current_mode = self.connection.current_mode();
|
||||
|
||||
let current_index = all_modes
|
||||
.iter()
|
||||
.position(|mode| mode.id.0 == current_mode.0)
|
||||
.unwrap_or(0);
|
||||
|
||||
let next_index = (current_index + 1) % all_modes.len();
|
||||
self.set_mode(all_modes[next_index].id.clone(), cx);
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
|
||||
let task = self.connection.set_mode(mode, cx);
|
||||
self.setting_mode = true;
|
||||
cx.notify();
|
||||
|
||||
cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
|
||||
if let Err(err) = task.await {
|
||||
log::error!("Failed to set session mode: {:?}", err);
|
||||
}
|
||||
this.update(cx, |this, cx| {
|
||||
this.setting_mode = false;
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
let weak_self = cx.weak_entity();
|
||||
|
||||
ContextMenu::build(window, cx, move |mut menu, _window, cx| {
|
||||
let all_modes = self.connection.all_modes();
|
||||
let current_mode = self.connection.current_mode();
|
||||
let default_mode = self.agent_server.default_mode(cx);
|
||||
|
||||
for mode in all_modes {
|
||||
let is_selected = &mode.id == ¤t_mode;
|
||||
let is_default = Some(&mode.id) == default_mode.as_ref();
|
||||
let entry = ContextMenuEntry::new(mode.name.clone())
|
||||
.toggleable(IconPosition::End, is_selected);
|
||||
|
||||
let entry = if let Some(description) = &mode.description {
|
||||
entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Bottom, {
|
||||
let description = description.clone();
|
||||
|
||||
move |cx| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(description.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.gap_0p5()
|
||||
.text_sm()
|
||||
.text_color(Color::Muted.color(cx))
|
||||
.child("Hold")
|
||||
.child(h_flex().flex_shrink_0().children(
|
||||
ui::render_modifiers(
|
||||
&gpui::Modifiers::secondary_key(),
|
||||
PlatformStyle::platform(),
|
||||
None,
|
||||
Some(ui::TextSize::Default.rems(cx).into()),
|
||||
true,
|
||||
),
|
||||
))
|
||||
.child(div().map(|this| {
|
||||
if is_default {
|
||||
this.child("to also unset as default")
|
||||
} else {
|
||||
this.child("to also set as default")
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
entry
|
||||
};
|
||||
|
||||
menu.push_item(entry.handler({
|
||||
let mode_id = mode.id.clone();
|
||||
let weak_self = weak_self.clone();
|
||||
move |window, cx| {
|
||||
weak_self
|
||||
.update(cx, |this, cx| {
|
||||
if window.modifiers().secondary() {
|
||||
this.agent_server.set_default_mode(
|
||||
if is_default {
|
||||
None
|
||||
} else {
|
||||
Some(mode_id.clone())
|
||||
},
|
||||
this.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
this.set_mode(mode_id.clone(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
menu.key_context("ModeSelector")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModeSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let current_mode_id = self.connection.current_mode();
|
||||
let current_mode_name = self
|
||||
.connection
|
||||
.all_modes()
|
||||
.iter()
|
||||
.find(|mode| mode.id == current_mode_id)
|
||||
.map(|mode| mode.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let this = cx.entity();
|
||||
|
||||
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(self.setting_mode);
|
||||
|
||||
PopoverMenu::new("mode-selector")
|
||||
.trigger_with_tooltip(
|
||||
trigger_button,
|
||||
Tooltip::element({
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
move |window, cx| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(Label::new("Cycle Through Modes"))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&CycleModeSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Toggle Mode Menu"))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&ToggleProfileSelector,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomRight)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(-2.0),
|
||||
})
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -192,8 +192,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.emit(DismissEvent);
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
|
||||
@@ -5,7 +5,8 @@ use agent_client_protocol as acp;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use ui::{
|
||||
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*,
|
||||
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
|
||||
prelude::*,
|
||||
};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
@@ -36,6 +37,14 @@ impl AcpModelSelectorPopover {
|
||||
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
|
||||
self.selector
|
||||
.read(cx)
|
||||
.delegate
|
||||
.active_model()
|
||||
.map(|model| model.name.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
@@ -50,15 +59,22 @@ impl Render for AcpModelSelectorPopover {
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let color = if self.menu_handle.is_deployed() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
.color(color)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
|
||||
@@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
|
||||
UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range};
|
||||
use text::Bias;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState,
|
||||
Tooltip, prelude::*,
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
pub struct AcpThreadHistory {
|
||||
@@ -26,8 +26,6 @@ pub struct AcpThreadHistory {
|
||||
|
||||
visible_items: Vec<ListItemType>,
|
||||
|
||||
scrollbar_visibility: bool,
|
||||
scrollbar_state: ScrollbarState,
|
||||
local_timezone: UtcOffset,
|
||||
|
||||
_update_task: Task<()>,
|
||||
@@ -70,7 +68,7 @@ impl AcpThreadHistory {
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", cx);
|
||||
editor.set_placeholder_text("Search threads...", window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -90,7 +88,6 @@ impl AcpThreadHistory {
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
history_store,
|
||||
@@ -99,8 +96,6 @@ impl AcpThreadHistory {
|
||||
hovered_index: None,
|
||||
visible_items: Default::default(),
|
||||
search_editor,
|
||||
scrollbar_visibility: true,
|
||||
scrollbar_state,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
@@ -339,43 +334,6 @@ impl AcpThreadHistory {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
|
||||
if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
div()
|
||||
.occlude()
|
||||
.id("thread-history-scroll")
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().panel_background.opacity(0.8))
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w_4()
|
||||
.pl_1()
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_list_items(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
@@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory {
|
||||
}
|
||||
|
||||
impl Render for AcpThreadHistory {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
@@ -542,22 +500,24 @@ impl Render for AcpThreadHistory {
|
||||
),
|
||||
)
|
||||
} else {
|
||||
view.pr_5()
|
||||
.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
view.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.when_some(self.render_scrollbar(cx), |div, scrollbar| {
|
||||
div.child(scrollbar)
|
||||
})
|
||||
.p_1()
|
||||
.pr_4()
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.flex_grow(),
|
||||
)
|
||||
.vertical_scrollbar_for(
|
||||
self.scroll_handle.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,10 @@ mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::Plan;
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
use collections::HashMap;
|
||||
use context_server::ContextServerId;
|
||||
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
|
||||
@@ -26,14 +25,17 @@ use language_model::{
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
agent_server_store::{
|
||||
AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
|
||||
CustomAgentServerSettings, GEMINI_NAME,
|
||||
},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
|
||||
Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
|
||||
prelude::*,
|
||||
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
@@ -45,11 +47,13 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
use crate::{
|
||||
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
placeholder_command,
|
||||
};
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
@@ -59,13 +63,13 @@ pub struct AgentConfiguration {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
_registry_subscription: Subscription,
|
||||
scroll_handle: ScrollHandle,
|
||||
scrollbar_state: ScrollbarState,
|
||||
_check_for_gemini: Task<()>,
|
||||
}
|
||||
|
||||
impl AgentConfiguration {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -95,22 +99,19 @@ impl AgentConfiguration {
|
||||
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
language_registry,
|
||||
workspace,
|
||||
focus_handle,
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
agent_server_store,
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
scrollbar_state,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
_check_for_gemini: Task::ready(()),
|
||||
};
|
||||
this.build_provider_configuration_views(window, cx);
|
||||
@@ -273,13 +274,28 @@ impl AgentConfiguration {
|
||||
*is_expanded = !*is_expanded;
|
||||
}
|
||||
})),
|
||||
)
|
||||
.when(provider.is_authenticated(cx), |parent| {
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
))),
|
||||
})
|
||||
.when(is_expanded && provider.is_authenticated(cx), |parent| {
|
||||
parent.child(
|
||||
Button::new(
|
||||
SharedString::from(format!("new-thread-{provider_id}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.full_width()
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon(IconName::Thread)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -296,17 +312,6 @@ impl AgentConfiguration {
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.when(is_expanded, |parent| match configuration_view {
|
||||
Some(configuration_view) => parent.child(configuration_view),
|
||||
None => parent.child(Label::new(format!(
|
||||
"No configuration view for {provider_name}",
|
||||
))),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_provider_configuration_section(
|
||||
@@ -508,9 +513,15 @@ impl AgentConfiguration {
|
||||
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
||||
|
||||
let (plan_name, label_color, bg_color) = match plan {
|
||||
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
|
||||
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
|
||||
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
|
||||
Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => {
|
||||
("Free", Color::Default, free_chip_bg)
|
||||
}
|
||||
Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => {
|
||||
("Pro Trial", Color::Accent, pro_chip_bg)
|
||||
}
|
||||
Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => {
|
||||
("Pro", Color::Accent, pro_chip_bg)
|
||||
}
|
||||
};
|
||||
|
||||
Chip::new(plan_name.to_string())
|
||||
@@ -554,11 +565,28 @@ impl AgentConfiguration {
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.children(
|
||||
context_server_ids.into_iter().map(|context_server_id| {
|
||||
self.render_context_server(context_server_id, window, cx)
|
||||
}),
|
||||
)
|
||||
.map(|parent| {
|
||||
if context_server_ids.is_empty() {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.p_4()
|
||||
.justify_center()
|
||||
.border_1()
|
||||
.border_dashed()
|
||||
.border_color(cx.theme().colors().border.opacity(0.6))
|
||||
.rounded_sm()
|
||||
.child(
|
||||
Label::new("No MCP servers added yet.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
parent.children(context_server_ids.into_iter().map(|context_server_id| {
|
||||
self.render_context_server(context_server_id, window, cx)
|
||||
}))
|
||||
}
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
@@ -811,6 +839,8 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(
|
||||
Disclosure::new(
|
||||
"tool-list-disclosure",
|
||||
@@ -834,17 +864,19 @@ impl AgentConfiguration {
|
||||
.id(SharedString::from(format!("tooltip-{}", item_id)))
|
||||
.h_full()
|
||||
.w_3()
|
||||
.mx_1()
|
||||
.ml_1()
|
||||
.mr_1p5()
|
||||
.justify_center()
|
||||
.tooltip(Tooltip::text(tooltip_text))
|
||||
.child(status_indicator),
|
||||
)
|
||||
.child(Label::new(item_id).ml_0p5())
|
||||
.child(Label::new(item_id).truncate())
|
||||
.child(
|
||||
div()
|
||||
.id("extension-source")
|
||||
.mt_0p5()
|
||||
.mx_1()
|
||||
.flex_none()
|
||||
.tooltip(Tooltip::text(source_tooltip))
|
||||
.child(
|
||||
Icon::new(source_icon)
|
||||
@@ -866,7 +898,8 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.gap_0p5()
|
||||
.flex_none()
|
||||
.child(context_server_configuration_menu)
|
||||
.child(
|
||||
Switch::new("context-server-switch", is_running.into())
|
||||
@@ -991,17 +1024,30 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||
let user_defined_agents = settings
|
||||
let custom_settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.iter()
|
||||
.map(|(name, settings)| {
|
||||
.clone();
|
||||
let user_defined_agents = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.external_agents()
|
||||
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let user_defined_agents = user_defined_agents
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
self.render_agent_server(
|
||||
IconName::Ai,
|
||||
name.clone(),
|
||||
ExternalAgent::Custom {
|
||||
name: name.clone(),
|
||||
command: settings.command.clone(),
|
||||
name: name.clone().into(),
|
||||
command: custom_settings
|
||||
.get(&name.0)
|
||||
.map(|settings| settings.command.clone())
|
||||
.unwrap_or(placeholder_command()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -1103,6 +1149,7 @@ impl AgentConfiguration {
|
||||
SharedString::from(format!("start_acp_thread-{name}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Thread)
|
||||
.icon_position(IconPosition::Start)
|
||||
@@ -1131,42 +1178,21 @@ impl Render for AgentConfiguration {
|
||||
.size_full()
|
||||
.pb_8()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.id("assistant-configuration-content")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("assistant-configuration-scrollbar")
|
||||
.occlude()
|
||||
.absolute()
|
||||
.right(px(3.))
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.pb_6()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.on_mouse_move(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_scroll_wheel(cx.listener(|_, _, _window, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone())),
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex()
|
||||
.id("assistant-configuration-content")
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
)
|
||||
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1300,6 +1326,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
args: vec![],
|
||||
env: Some(HashMap::default()),
|
||||
},
|
||||
default_mode: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -251,6 +251,7 @@ pub struct ConfigureContextServerModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
source: ConfigurationSource,
|
||||
state: State,
|
||||
original_server_id: Option<ContextServerId>,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
@@ -348,6 +349,11 @@ impl ConfigureContextServerModal {
|
||||
context_server_store,
|
||||
workspace: workspace_handle,
|
||||
state: State::Idle,
|
||||
original_server_id: match &target {
|
||||
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::New => None,
|
||||
},
|
||||
source: ConfigurationSource::from_target(
|
||||
target,
|
||||
language_registry,
|
||||
@@ -415,9 +421,19 @@ impl ConfigureContextServerModal {
|
||||
// When we write the settings to the file, the context server will be restarted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
});
|
||||
let original_server_id = self.original_server_id.clone();
|
||||
update_settings_file::<ProjectSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |project_settings, _| {
|
||||
if let Some(original_id) = original_server_id {
|
||||
if original_id != id {
|
||||
project_settings.context_servers.remove(&original_id.0);
|
||||
}
|
||||
}
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if let Some(existing_server) = existing_server {
|
||||
self.context_server_store
|
||||
|
||||
@@ -2,7 +2,7 @@ mod profile_modal_header;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_settings::{AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
@@ -16,7 +16,6 @@ use workspace::{ModalView, Workspace};
|
||||
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
|
||||
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AgentPanel, ManageProfiles};
|
||||
use agent::agent_profile::AgentProfile;
|
||||
|
||||
use super::tool_picker::ToolPickerMode;
|
||||
|
||||
@@ -156,7 +155,7 @@ impl ManageProfilesModal {
|
||||
) {
|
||||
let name_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
name_editor.update(cx, |editor, cx| {
|
||||
editor.set_placeholder_text("Profile name", cx);
|
||||
editor.set_placeholder_text("Profile name", window, cx);
|
||||
});
|
||||
|
||||
self.mode = Mode::NewProfile(NewProfileMode {
|
||||
|
||||
@@ -318,7 +318,7 @@ impl PickerDelegate for ToolPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let item = &self.filtered_items[ix];
|
||||
let item = &self.filtered_items.get(ix)?;
|
||||
match item {
|
||||
PickerItem::ContextServer { server_id, .. } => Some(
|
||||
div()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use acp_thread::{AcpThread, AcpThreadEvent};
|
||||
use action_log::ActionLog;
|
||||
use agent::{Thread, ThreadEvent, ThreadSummary};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
@@ -19,7 +18,6 @@ use gpui::{
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
use language_model::StopReason;
|
||||
use multi_buffer::PathKey;
|
||||
use project::{Project, ProjectItem, ProjectPath};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -51,34 +49,29 @@ pub struct AgentDiffPane {
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum AgentDiffThread {
|
||||
Native(Entity<Thread>),
|
||||
AcpThread(Entity<AcpThread>),
|
||||
}
|
||||
|
||||
impl AgentDiffThread {
|
||||
fn project(&self, cx: &App) -> Entity<Project> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).project().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
|
||||
}
|
||||
}
|
||||
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(&self, cx: &App) -> ThreadSummary {
|
||||
fn title(&self, cx: &App) -> SharedString {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(),
|
||||
AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_generating(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).is_generating(),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
thread.read(cx).status() == acp_thread::ThreadStatus::Generating
|
||||
}
|
||||
@@ -87,14 +80,12 @@ impl AgentDiffThread {
|
||||
|
||||
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(),
|
||||
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakAgentDiffThread {
|
||||
match self {
|
||||
AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()),
|
||||
AgentDiffThread::AcpThread(thread) => {
|
||||
WeakAgentDiffThread::AcpThread(thread.downgrade())
|
||||
}
|
||||
@@ -102,12 +93,6 @@ impl AgentDiffThread {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<Thread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<Thread>) -> Self {
|
||||
AgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
fn from(entity: Entity<AcpThread>) -> Self {
|
||||
AgentDiffThread::AcpThread(entity)
|
||||
@@ -116,25 +101,17 @@ impl From<Entity<AcpThread>> for AgentDiffThread {
|
||||
|
||||
#[derive(PartialEq, Eq, Clone)]
|
||||
pub enum WeakAgentDiffThread {
|
||||
Native(WeakEntity<Thread>),
|
||||
AcpThread(WeakEntity<AcpThread>),
|
||||
}
|
||||
|
||||
impl WeakAgentDiffThread {
|
||||
pub fn upgrade(&self) -> Option<AgentDiffThread> {
|
||||
match self {
|
||||
WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native),
|
||||
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<Thread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<Thread>) -> Self {
|
||||
WeakAgentDiffThread::Native(entity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
|
||||
fn from(entity: WeakEntity<AcpThread>) -> Self {
|
||||
WeakAgentDiffThread::AcpThread(entity)
|
||||
@@ -203,10 +180,6 @@ impl AgentDiffPane {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
match &thread {
|
||||
AgentDiffThread::Native(thread) => cx
|
||||
.subscribe(thread, |this, _thread, event, cx| {
|
||||
this.handle_native_thread_event(event, cx)
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx
|
||||
.subscribe(thread, |this, _thread, event, cx| {
|
||||
this.handle_acp_thread_event(event, cx)
|
||||
@@ -313,19 +286,13 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.title(cx);
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_native_thread_event(&mut self, event: &ThreadEvent, cx: &mut Context<Self>) {
|
||||
if let ThreadEvent::SummaryGenerated = event {
|
||||
self.update_title(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(&mut self, event: &AcpThreadEvent, cx: &mut Context<Self>) {
|
||||
if let AcpThreadEvent::TitleUpdated = event {
|
||||
self.update_title(cx)
|
||||
@@ -569,8 +536,8 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.thread.summary(cx).unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
let title = self.thread.title(cx);
|
||||
Label::new(format!("Review: {}", title))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
@@ -1339,12 +1306,6 @@ impl AgentDiff {
|
||||
});
|
||||
|
||||
let thread_subscription = match &thread {
|
||||
AgentDiffThread::Native(thread) => cx.subscribe_in(thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_native_thread_event(&workspace, event, window, cx)
|
||||
}
|
||||
}),
|
||||
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, thread, event, window, cx| {
|
||||
@@ -1447,47 +1408,6 @@ impl AgentDiff {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_native_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
event: &ThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadEvent::NewRequest
|
||||
| ThreadEvent::Stopped(Ok(StopReason::EndTurn))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::MaxTokens))
|
||||
| ThreadEvent::Stopped(Ok(StopReason::Refusal))
|
||||
| ThreadEvent::Stopped(Err(_))
|
||||
| ThreadEvent::ShowError(_)
|
||||
| ThreadEvent::CompletionCanceled => {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
// intentionally being exhaustive in case we add a variant we should handle
|
||||
ThreadEvent::Stopped(Ok(StopReason::ToolUse))
|
||||
| ThreadEvent::StreamedCompletion
|
||||
| ThreadEvent::ReceivedTextChunk
|
||||
| ThreadEvent::StreamedAssistantText(_, _)
|
||||
| ThreadEvent::StreamedAssistantThinking(_, _)
|
||||
| ThreadEvent::StreamedToolUse { .. }
|
||||
| ThreadEvent::InvalidToolInput { .. }
|
||||
| ThreadEvent::MissingToolUse { .. }
|
||||
| ThreadEvent::MessageAdded(_)
|
||||
| ThreadEvent::MessageEdited(_)
|
||||
| ThreadEvent::MessageDeleted(_)
|
||||
| ThreadEvent::SummaryGenerated
|
||||
| ThreadEvent::SummaryChanged
|
||||
| ThreadEvent::UsePendingTools { .. }
|
||||
| ThreadEvent::ToolFinished { .. }
|
||||
| ThreadEvent::CheckpointChanged
|
||||
| ThreadEvent::ToolConfirmationNeeded
|
||||
| ThreadEvent::ToolUseLimitReached
|
||||
| ThreadEvent::CancelEditing
|
||||
| ThreadEvent::ProfileChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_acp_thread_event(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
@@ -1517,7 +1437,10 @@ impl AgentDiff {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => {
|
||||
AcpThreadEvent::Stopped
|
||||
| AcpThreadEvent::Error
|
||||
| AcpThreadEvent::LoadError(_)
|
||||
| AcpThreadEvent::Refusal => {
|
||||
self.update_reviewing_editors(workspace, window, cx);
|
||||
}
|
||||
AcpThreadEvent::TitleUpdated
|
||||
@@ -1525,7 +1448,9 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::EntriesRemoved(_)
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::Retry(_) => {}
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_)
|
||||
| AcpThreadEvent::ModeUpdated(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1885,16 +1810,14 @@ impl editor::Addon for EditorAgentDiffAddon {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Keep;
|
||||
use agent::thread_store::{self, ThreadStore};
|
||||
use acp_thread::AgentConnection as _;
|
||||
use agent_settings::AgentSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use editor::EditorSettings;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::{path::Path, rc::Rc};
|
||||
use theme::ThemeSettings;
|
||||
use util::path;
|
||||
|
||||
@@ -1907,7 +1830,6 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
@@ -1927,21 +1849,17 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
let connection = Rc::new(acp_thread::StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("/test")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread =
|
||||
AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx)));
|
||||
|
||||
let thread = AgentDiffThread::AcpThread(thread);
|
||||
let action_log = cx.read(|cx| thread.action_log(cx));
|
||||
|
||||
let (workspace, cx) =
|
||||
@@ -2064,7 +1982,6 @@ mod tests {
|
||||
Project::init_settings(cx);
|
||||
AgentSettings::register(cx);
|
||||
prompt_store::init(cx);
|
||||
thread_store::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
@@ -2093,22 +2010,6 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let prompt_store = None;
|
||||
let thread_store = cx
|
||||
.update(|cx| {
|
||||
ThreadStore::load(
|
||||
project.clone(),
|
||||
cx.new(|_| ToolWorkingSet::default()),
|
||||
prompt_store,
|
||||
Arc::new(PromptBuilder::new(None).unwrap()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
@@ -2127,8 +2028,19 @@ mod tests {
|
||||
}
|
||||
});
|
||||
|
||||
let connection = Rc::new(acp_thread::StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|_, cx| {
|
||||
connection
|
||||
.clone()
|
||||
.new_thread(project.clone(), Path::new(path!("/test")), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
// Set the active thread
|
||||
let thread = AgentDiffThread::Native(thread);
|
||||
let thread = AgentDiffThread::AcpThread(thread);
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::{
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
@@ -39,28 +38,6 @@ impl AgentModelSelector {
|
||||
let provider = model.provider_id().0.to_string();
|
||||
let model_id = model.id().0.to_string();
|
||||
match &model_usage_context {
|
||||
ModelUsageContext::Thread(thread) => {
|
||||
thread.update(cx, |thread, cx| {
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
if let Some(provider) = registry.provider(&model.provider_id())
|
||||
{
|
||||
thread.set_configured_model(
|
||||
Some(ConfiguredModel {
|
||||
provider,
|
||||
model: model.clone(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |settings, _cx| {
|
||||
settings.set_model(model.clone());
|
||||
},
|
||||
);
|
||||
}
|
||||
ModelUsageContext::InlineAssistant => {
|
||||
update_settings_file::<AgentSettings>(
|
||||
fs.clone(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
mod acp;
|
||||
mod active_thread;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
@@ -8,7 +7,6 @@ mod buffer_codegen;
|
||||
mod context_picker;
|
||||
mod context_server_configuration;
|
||||
mod context_strip;
|
||||
mod debug;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod language_model_selector;
|
||||
@@ -20,15 +18,12 @@ mod slash_command_settings;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod text_thread_editor;
|
||||
mod thread_history;
|
||||
mod tool_compatibility;
|
||||
mod ui;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_servers::AgentServerCommand;
|
||||
use agent::ThreadId;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
@@ -41,20 +36,19 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::any::TypeId;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
use crate::slash_command_settings::SlashCommandSettings;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
pub use ui::preview::{all_agent_previews, get_agent_preview};
|
||||
use zed_actions;
|
||||
|
||||
actions!(
|
||||
@@ -72,8 +66,10 @@ actions!(
|
||||
ToggleOptionsMenu,
|
||||
/// Deletes the recently opened thread from history.
|
||||
DeleteRecentlyOpenThread,
|
||||
/// Toggles the profile selector for switching between agent profiles.
|
||||
/// Toggles the profile or mode selector for switching between agent profiles.
|
||||
ToggleProfileSelector,
|
||||
/// Cycles through available session modes.
|
||||
CycleModeSelector,
|
||||
/// Removes all added context from the current conversation.
|
||||
RemoveAllContext,
|
||||
/// Expands the message editor to full size.
|
||||
@@ -114,6 +110,12 @@ actions!(
|
||||
RejectAll,
|
||||
/// Keeps all suggestions or changes.
|
||||
KeepAll,
|
||||
/// Allow this operation only this time.
|
||||
AllowOnce,
|
||||
/// Allow this operation and remember the choice.
|
||||
AllowAlways,
|
||||
/// Reject this operation only this time.
|
||||
RejectOnce,
|
||||
/// Follows the agent's suggestions.
|
||||
Follow,
|
||||
/// Resets the trial upsell notification.
|
||||
@@ -174,6 +176,14 @@ enum ExternalAgent {
|
||||
},
|
||||
}
|
||||
|
||||
fn placeholder_command() -> AgentServerCommand {
|
||||
AgentServerCommand {
|
||||
path: "/placeholder".into(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -193,10 +203,9 @@ impl ExternalAgent {
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new(
|
||||
name.clone(),
|
||||
command.clone(),
|
||||
)),
|
||||
Self::Custom { name, command: _ } => {
|
||||
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,14 +229,12 @@ impl ManageProfiles {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ModelUsageContext {
|
||||
Thread(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
impl ModelUsageContext {
|
||||
pub fn configured_model(&self, cx: &App) -> Option<ConfiguredModel> {
|
||||
match self {
|
||||
Self::Thread(thread) => thread.read(cx).configured_model(),
|
||||
Self::InlineAssistant => {
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
}
|
||||
@@ -337,8 +344,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter
|
||||
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1139,7 +1139,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
@@ -1208,7 +1208,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
@@ -1277,7 +1277,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
|
||||
@@ -6,7 +6,7 @@ pub(crate) mod symbol_context_picker;
|
||||
pub(crate) mod thread_context_picker;
|
||||
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -23,9 +23,8 @@ use gpui::{
|
||||
};
|
||||
use language::Buffer;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use paths::contexts_dir;
|
||||
use project::{Entry, ProjectPath};
|
||||
use prompt_store::{PromptStore, UserPromptId};
|
||||
use project::ProjectPath;
|
||||
use prompt_store::PromptStore;
|
||||
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
|
||||
use symbol_context_picker::SymbolContextPicker;
|
||||
use thread_context_picker::{
|
||||
@@ -34,10 +33,8 @@ use thread_context_picker::{
|
||||
use ui::{
|
||||
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use workspace::{Workspace, notifications::NotifyResultExt};
|
||||
|
||||
use crate::AgentPanel;
|
||||
use agent::{
|
||||
ThreadId,
|
||||
context::RULES_ICON,
|
||||
@@ -664,7 +661,7 @@ pub(crate) fn recent_context_picker_entries(
|
||||
text_thread_store: Option<WeakEntity<TextThreadStore>>,
|
||||
workspace: Entity<Workspace>,
|
||||
exclude_paths: &HashSet<PathBuf>,
|
||||
exclude_threads: &HashSet<ThreadId>,
|
||||
_exclude_threads: &HashSet<ThreadId>,
|
||||
cx: &App,
|
||||
) -> Vec<RecentEntry> {
|
||||
let mut recent = Vec::with_capacity(6);
|
||||
@@ -690,19 +687,13 @@ pub(crate) fn recent_context_picker_entries(
|
||||
}),
|
||||
);
|
||||
|
||||
let active_thread_id = workspace
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
|
||||
|
||||
if let Some((thread_store, text_thread_store)) = thread_store
|
||||
.and_then(|store| store.upgrade())
|
||||
.zip(text_thread_store.and_then(|store| store.upgrade()))
|
||||
{
|
||||
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
|
||||
.filter(|(_, thread)| match thread {
|
||||
ThreadContextEntry::Thread { id, .. } => {
|
||||
Some(id) != active_thread_id && !exclude_threads.contains(id)
|
||||
}
|
||||
ThreadContextEntry::Thread { .. } => false,
|
||||
ThreadContextEntry::Context { .. } => true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -874,15 +865,7 @@ fn fold_toggle(
|
||||
}
|
||||
}
|
||||
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Selection(ProjectPath, Range<usize>),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
TextThread(Arc<Path>),
|
||||
Rule(UserPromptId),
|
||||
}
|
||||
pub struct MentionLink;
|
||||
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
@@ -894,17 +877,6 @@ impl MentionLink {
|
||||
|
||||
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
|
||||
|
||||
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::SELECTION)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
|| url.starts_with(Self::RULE)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
||||
}
|
||||
@@ -958,74 +930,4 @@ impl MentionLink {
|
||||
pub fn for_rule(rule: &RulesContextEntry) -> String {
|
||||
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
|
||||
}
|
||||
|
||||
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::SELECTION => {
|
||||
let (path, line_args) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
|
||||
let line_range = {
|
||||
let (start, end) = line_args
|
||||
.trim_start_matches('(')
|
||||
.trim_end_matches(')')
|
||||
.split_once('-')?;
|
||||
start.parse::<usize>().ok()?..end.parse::<usize>().ok()?
|
||||
};
|
||||
|
||||
Some(MentionLink::Selection(project_path, line_range))
|
||||
}
|
||||
Self::THREAD => {
|
||||
if let Some(encoded_filename) = argument.strip_prefix(Self::TEXT_THREAD_URL_PREFIX)
|
||||
{
|
||||
let filename = urlencoding::decode(encoded_filename).ok()?;
|
||||
let path = contexts_dir().join(filename.as_ref()).into();
|
||||
Some(MentionLink::TextThread(path))
|
||||
} else {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
Self::RULE => {
|
||||
let prompt_id = UserPromptId(Uuid::try_parse(argument).ok()?);
|
||||
Some(MentionLink::Rule(prompt_id))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ use http_client::HttpClientWithUrl;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
|
||||
Symbol, WorktreeId,
|
||||
};
|
||||
use prompt_store::PromptStore;
|
||||
use rope::Point;
|
||||
use text::{Anchor, OffsetRangeExt, ToPoint};
|
||||
@@ -593,11 +596,12 @@ impl ContextPickerCompletionProvider {
|
||||
file_name.to_string()
|
||||
};
|
||||
|
||||
let path = Path::new(&full_path);
|
||||
let crease_icon_path = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
|
||||
FileIcons::get_folder_icon(false, path, cx)
|
||||
.unwrap_or_else(|| IconName::Folder.path().into())
|
||||
} else {
|
||||
FileIcons::get_icon(Path::new(&full_path), cx)
|
||||
.unwrap_or_else(|| IconName::File.path().into())
|
||||
FileIcons::get_icon(path, cx).unwrap_or_else(|| IconName::File.path().into())
|
||||
};
|
||||
let completion_icon_path = if is_recent {
|
||||
IconName::HistoryRerun.path().into()
|
||||
@@ -897,6 +901,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
// Since this does its own filtering (see `filter_completions()` returns false),
|
||||
// there is no benefit to computing whether this set of completions is incomplete.
|
||||
is_incomplete: true,
|
||||
|
||||
@@ -160,7 +160,7 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let FileMatch { mat, .. } = &self.matches[ix];
|
||||
let FileMatch { mat, .. } = &self.matches.get(ix)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
@@ -330,7 +330,7 @@ pub fn render_file_context_entry(
|
||||
});
|
||||
|
||||
let file_icon = if is_directory {
|
||||
FileIcons::get_folder_icon(false, cx)
|
||||
FileIcons::get_folder_icon(false, path, cx)
|
||||
} else {
|
||||
FileIcons::get_icon(path, cx)
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ impl PickerDelegate for RulesContextPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
let thread = &self.matches.get(ix)?;
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
||||
|
||||
@@ -169,7 +169,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
let mat = &self.matches.get(ix)?;
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
|
||||
|
||||
@@ -220,7 +220,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let thread = &self.matches[ix];
|
||||
let thread = &self.matches.get(ix)?;
|
||||
|
||||
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
render_thread_context_entry(thread, self.context_store.clone(), cx),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user