Compare commits
393 Commits
v0.201.8
...
agent-serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d838bb14 | ||
|
|
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 | ||
|
|
7e3fbeb59d | ||
|
|
8e7caa429d | ||
|
|
c894351544 | ||
|
|
a96015b3c5 | ||
|
|
2eb7ac97e0 | ||
|
|
f06c18765f | ||
|
|
2f279c5de4 | ||
|
|
60b95d9253 | ||
|
|
47ad1b2143 | ||
|
|
35c0d02c7c | ||
|
|
374a8bc4cb | ||
|
|
f06be6f3ec | ||
|
|
970242480a | ||
|
|
54cec5b484 | ||
|
|
60d17cccd3 | ||
|
|
8a8a9a4f07 | ||
|
|
634a1343dd | ||
|
|
2ba25b5c94 | ||
|
|
965dbc988f | ||
|
|
5b73b40df8 | ||
|
|
d910feac1d | ||
|
|
61175ab9cd | ||
|
|
2790eb604a | ||
|
|
acff65ed3f | ||
|
|
3315fd94d2 | ||
|
|
62083fe796 | ||
|
|
a852bcc094 | ||
|
|
f290daf7ea | ||
|
|
129bff8358 | ||
|
|
c833f8905b | ||
|
|
d74384f6e2 | ||
|
|
5abc398a0a | ||
|
|
9c8c3966df | ||
|
|
e48be30266 | ||
|
|
babc0c09f0 | ||
|
|
39d41ed822 | ||
|
|
b69ebbd7b7 | ||
|
|
f348737e8c | ||
|
|
1ca5e84019 | ||
|
|
d80f13242b | ||
|
|
e115584896 | ||
|
|
fe0ab30e8f | ||
|
|
253765aaa1 | ||
|
|
ad746f25f2 | ||
|
|
de576bd1b8 | ||
|
|
af26b627bf | ||
|
|
0a32aa8db1 | ||
|
|
b473f4a130 | ||
|
|
7d0a303785 | ||
|
|
f78f3e7729 | ||
|
|
1c2e2a00fe | ||
|
|
a70cf3f1d4 | ||
|
|
bdedb18c30 | ||
|
|
db508bbbe2 | ||
|
|
515282d719 | ||
|
|
f2c3f3b168 | ||
|
|
e9252a7a74 | ||
|
|
fcc3d1092f | ||
|
|
a790e514af | ||
|
|
92f739dbb9 | ||
|
|
3d4f917204 | ||
|
|
a13881746a | ||
|
|
11fb57a6d9 | ||
|
|
5001c03711 | ||
|
|
20d32d111c | ||
|
|
ff035e8a22 | ||
|
|
01266d10d6 | ||
|
|
4507f60b8d | ||
|
|
d13ba0162a | ||
|
|
7403a4ba17 | ||
|
|
52da72d80a | ||
|
|
384ffb883f | ||
|
|
c3ccdc0b44 | ||
|
|
e5cea54cbb | ||
|
|
cfd56a744d | ||
|
|
960d9ce48c | ||
|
|
52d119b637 | ||
|
|
8c18f059f1 | ||
|
|
930189ed83 | ||
|
|
08c23c92ca | ||
|
|
88e8f7af68 | ||
|
|
f2e62c98d1 | ||
|
|
8697b91ea0 | ||
|
|
47aaaa8bcf | ||
|
|
69933d5b81 | ||
|
|
909d7215c0 | ||
|
|
27777d4b8f | ||
|
|
4469b14512 | ||
|
|
29fc324a78 | ||
|
|
4ef9294123 | ||
|
|
4b0609840b | ||
|
|
2cb697e9f4 | ||
|
|
c8e99125bd | ||
|
|
835e5ba662 | ||
|
|
24ee98b3e1 | ||
|
|
213ee32b94 | ||
|
|
f127ba82d1 | ||
|
|
39d86eeb7f | ||
|
|
4981c33bf3 | ||
|
|
54609d4d00 | ||
|
|
ff03dda90a | ||
|
|
73b38c8306 | ||
|
|
38e5c8fb66 | ||
|
|
78c2f1621d | ||
|
|
0a9f407872 | ||
|
|
4e1a901059 | ||
|
|
8af212e785 | ||
|
|
b233df8343 | ||
|
|
9a97f9465b | ||
|
|
48299b5b24 | ||
|
|
4e4bfd6f4e | ||
|
|
5444fbd8fe | ||
|
|
58f896e5cd | ||
|
|
d43cf2c486 | ||
|
|
e2bf8e5d9c | ||
|
|
c158eb2442 | ||
|
|
71f900346c | ||
|
|
9ca4fb16b2 | ||
|
|
45ff22f793 | ||
|
|
fead511df9 | ||
|
|
07373d15ef | ||
|
|
b5e9b65e8c | ||
|
|
5d7f12ce88 | ||
|
|
1b9c471204 | ||
|
|
8cf663011f | ||
|
|
54f9b67de2 | ||
|
|
d99a17e357 | ||
|
|
c72e594afe | ||
|
|
b4d4294bee | ||
|
|
e5c0614e88 | ||
|
|
ea347b0aa1 | ||
|
|
a03897012e | ||
|
|
f4071bdd8e | ||
|
|
abd6009b41 | ||
|
|
a3e1611fa8 | ||
|
|
e6e64017ea | ||
|
|
d0aef3cec1 | ||
|
|
1eae76e856 | ||
|
|
d713390366 | ||
|
|
9614b72b06 | ||
|
|
d7c735959e | ||
|
|
d8847192c8 | ||
|
|
bd4e943597 | ||
|
|
c5d3c7d790 | ||
|
|
fff0ecead1 | ||
|
|
b1b60bb7fe | ||
|
|
0e575b2809 | ||
|
|
65c6c709fd | ||
|
|
858ab9cc23 | ||
|
|
2c64b05ea4 | ||
|
|
b7dad2cf71 | ||
|
|
76dbcde628 | ||
|
|
aa0f7a2d09 | ||
|
|
372b3c7af6 | ||
|
|
10a1140d49 | ||
|
|
e96b68bc15 | ||
|
|
b249593abe | ||
|
|
c14d84cfdb | ||
|
|
428fc6d483 | ||
|
|
64b14ef848 | ||
|
|
bf5ed6d1c9 | ||
|
|
bb5cfe118f | ||
|
|
633ce23ae9 | ||
|
|
d43df9e841 | ||
|
|
f8667a8379 | ||
|
|
1460573dd4 | ||
|
|
65de969cc8 | ||
|
|
628a9cd8ea | ||
|
|
ad25aba990 | ||
|
|
99cee8778c | ||
|
|
823a0018e5 | ||
|
|
9cc006ff74 | ||
|
|
0470baca50 | ||
|
|
4605b96630 | ||
|
|
949398cb93 | ||
|
|
79e74b880b | ||
|
|
59af2a7d1f | ||
|
|
c786c0150f | ||
|
|
5fd29d37a6 | ||
|
|
f1204dfc33 | ||
|
|
2e1ca47241 | ||
|
|
5c346a4ccf | ||
|
|
a102b08743 | ||
|
|
2dc4f156b3 | ||
|
|
557753d092 | ||
|
|
65fb17e2c9 | ||
|
|
2fe3dbed31 | ||
|
|
fda5111dc0 | ||
|
|
69127d2bea | ||
|
|
db949546cf | ||
|
|
2b5a302972 | ||
|
|
4c0ad95acc | ||
|
|
8c83281399 | ||
|
|
dfc99de7b8 | ||
|
|
fe5e81203f | ||
|
|
c48197b280 | ||
|
|
11545c669e | ||
|
|
a79aef7bdd | ||
|
|
d8bffd7ef2 | ||
|
|
54c7d9dc5f | ||
|
|
dd6fce6d4e | ||
|
|
de5f87e8f2 | ||
|
|
1b91f3de41 | ||
|
|
19764794b7 | ||
|
|
d49409caba | ||
|
|
60ea4754b2 | ||
|
|
61bc1cc441 | ||
|
|
70575d1115 | ||
|
|
ea42013746 | ||
|
|
5da31fdb72 | ||
|
|
f48a8f2b6a | ||
|
|
d24cad30f3 | ||
|
|
153724aad3 | ||
|
|
bc566fe18e | ||
|
|
91b2a84001 | ||
|
|
e6267c42f7 | ||
|
|
f649c31bf9 | ||
|
|
639417c2bc | ||
|
|
896a35f7be | ||
|
|
4560d1ec58 | ||
|
|
18ac4ac5ef | ||
|
|
72bd248544 | ||
|
|
42ae3301d0 | ||
|
|
eb0f9ddcdc | ||
|
|
ac9fdaa1da | ||
|
|
8204ef1e51 | ||
|
|
3d2fa72d1f | ||
|
|
92bbcdeb7d | ||
|
|
54df43e06f | ||
|
|
4f0fad6996 | ||
|
|
3b7c1744b4 | ||
|
|
27a26d53b1 | ||
|
|
d88fd00e87 | ||
|
|
f4ba7997a7 | ||
|
|
e360691106 | ||
|
|
b349a8f34c | ||
|
|
e15856a37f | ||
|
|
852439452c | ||
|
|
f5fd4ac670 | ||
|
|
e1a96b68f0 | ||
|
|
ca139b701e | ||
|
|
eeaadc098f | ||
|
|
18fe68d991 | ||
|
|
a977fbc5b0 | ||
|
|
06c0e59379 | ||
|
|
0beb919bbb | ||
|
|
20a0c3e920 | ||
|
|
731b5d0def | ||
|
|
555692fac6 | ||
|
|
2234f91b7b | ||
|
|
725ed5dd01 | ||
|
|
d0583ede48 | ||
|
|
33e05f15b2 | ||
|
|
c1e749906f | ||
|
|
81cb24810b | ||
|
|
f2899bf34b | ||
|
|
1b2ceae7ef | ||
|
|
d166ab95a1 | ||
|
|
b284b1a0b8 | ||
|
|
6f32d36ec9 | ||
|
|
190217a43b | ||
|
|
132daef9f6 | ||
|
|
4bee06e507 | ||
|
|
f23314bef4 | ||
|
|
697a39c251 | ||
|
|
d9ea97ee9c | ||
|
|
d8fc779a67 | ||
|
|
001ec97c0e | ||
|
|
2781a30971 | ||
|
|
e0613cbd0f | ||
|
|
1dd237139c | ||
|
|
f63d8e4c53 | ||
|
|
ad64a71f04 | ||
|
|
f435af2fde | ||
|
|
c5ee3f3e2e | ||
|
|
7f1bd2f15e | ||
|
|
62f2ef86dc | ||
|
|
fda6eda3c2 | ||
|
|
ed84767c9d | ||
|
|
cde0a5dd27 | ||
|
|
68f97d6069 | ||
|
|
5dcb90858e | ||
|
|
c731bb6d91 | ||
|
|
4b03d791b5 | ||
|
|
9a3e4c47d0 | ||
|
|
568e1d0a42 | ||
|
|
6f242772cc | ||
|
|
8ef9ecc91f | ||
|
|
3dd362978a | ||
|
|
74c0ba980b | ||
|
|
c20233e0b4 | ||
|
|
ffb995181e | ||
|
|
5120b6b7f9 | ||
|
|
c9c708ff08 | ||
|
|
9e34bb3f05 | ||
|
|
595cf1c6c3 | ||
|
|
d1820b183a | ||
|
|
fb7edbfb46 | ||
|
|
02dabbb9fa | ||
|
|
fa8bef1496 | ||
|
|
739e4551da | ||
|
|
b0bef3a9a2 |
@@ -41,5 +41,4 @@ workspace-members = [
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
"zed_toml",
|
||||
]
|
||||
|
||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -1,2 +1,5 @@
|
||||
# Prevent GitHub from displaying comments within JSON files as errors.
|
||||
*.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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
@@ -14,7 +14,7 @@ body:
|
||||
### Description
|
||||
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
|
||||
- Any code must be sufficient to reproduce (include context!)
|
||||
- Code must as text, not just as a screenshot.
|
||||
- Include code as text, not just as a screenshot.
|
||||
- Issues with insufficient detail may be summarily closed.
|
||||
-->
|
||||
|
||||
|
||||
15
.github/actionlint.yml
vendored
15
.github/actionlint.yml
vendored
@@ -19,14 +19,27 @@ self-hosted-runner:
|
||||
- namespace-profile-16x32-ubuntu-2004-arm
|
||||
- namespace-profile-32x64-ubuntu-2004-arm
|
||||
# Namespace Ubuntu 22.04 (Everything else)
|
||||
- namespace-profile-2x4-ubuntu-2204
|
||||
- namespace-profile-4x8-ubuntu-2204
|
||||
- namespace-profile-8x16-ubuntu-2204
|
||||
- namespace-profile-16x32-ubuntu-2204
|
||||
- namespace-profile-32x64-ubuntu-2204
|
||||
# Namespace Ubuntu 24.04 (like ubuntu-latest)
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
# Namespace Limited Preview
|
||||
- namespace-profile-8x16-ubuntu-2004-arm-m4
|
||||
- namespace-profile-8x32-ubuntu-2004-arm-m4
|
||||
# Self Hosted Runners
|
||||
- self-mini-macos
|
||||
- self-32vcpu-windows-2022
|
||||
|
||||
# Disable shellcheck because it doesn't like powershell
|
||||
# This should have been triggered with initial rollout of actionlint
|
||||
# but https://github.com/zed-industries/zed/pull/36693
|
||||
# somehow caused actionlint to actually check those windows jobs
|
||||
# where previously they were being skipped. Likely caused by an
|
||||
# unknown bug in actionlint where parsing of `runs-on: [ ]`
|
||||
# breaks something else. (yuck)
|
||||
paths:
|
||||
.github/workflows/{ci,release_nightly}.yml:
|
||||
ignore:
|
||||
- "shellcheck"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
2
.github/workflows/bump_collab_staging.yml
vendored
2
.github/workflows/bump_collab_staging.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
update-collab-staging-tag:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -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"
|
||||
|
||||
@@ -237,7 +238,7 @@ jobs:
|
||||
uses: ./.github/actions/build_docs
|
||||
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
|
||||
needs: [job_spec]
|
||||
steps:
|
||||
@@ -418,7 +419,7 @@ jobs:
|
||||
if: |
|
||||
github.repository_owner == 'zed-industries' &&
|
||||
needs.job_spec.outputs.run_tests == 'true'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
steps:
|
||||
- name: Environment Setup
|
||||
run: |
|
||||
@@ -458,7 +459,7 @@ jobs:
|
||||
|
||||
tests_pass:
|
||||
name: Tests Pass
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
needs:
|
||||
- job_spec
|
||||
- style
|
||||
@@ -784,7 +785,7 @@ jobs:
|
||||
bundle-windows-x64:
|
||||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
needs: [windows_tests]
|
||||
|
||||
2
.github/workflows/danger.yml
vendored
2
.github/workflows/danger.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
jobs:
|
||||
danger:
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
10
.github/workflows/release_nightly.yml
vendored
10
.github/workflows/release_nightly.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Run tests on Windows
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
@@ -206,9 +206,6 @@ jobs:
|
||||
runs-on: github-8vcpu-ubuntu-2404
|
||||
needs: tests
|
||||
name: Build Zed on FreeBSD
|
||||
# env:
|
||||
# MYTOKEN : ${{ secrets.MYTOKEN }}
|
||||
# MYTOKEN2: "value2"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build FreeBSD remote-server
|
||||
@@ -243,7 +240,6 @@ jobs:
|
||||
|
||||
bundle-nix:
|
||||
name: Build and cache Nix package
|
||||
if: false
|
||||
needs: tests
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/nix.yml
|
||||
@@ -252,7 +248,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Create a Windows installer
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
runs-on: [self-32vcpu-windows-2022]
|
||||
needs: windows-tests
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||
@@ -294,7 +290,7 @@ jobs:
|
||||
update-nightly-tag:
|
||||
name: Update nightly tag
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
needs:
|
||||
- bundle-mac
|
||||
- bundle-linux-x86
|
||||
|
||||
2
.github/workflows/script_checks.yml
vendored
2
.github/workflows/script_checks.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
shellcheck:
|
||||
name: "ShellCheck Scripts"
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
13
.rules
13
.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
|
||||
|
||||
|
||||
@@ -27,6 +27,22 @@ By effectively engaging with the Zed team and community early in your process, w
|
||||
|
||||
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.
|
||||
|
||||
## Mandatory PR contents
|
||||
|
||||
Please ensure the PR contains
|
||||
|
||||
- Before & after screenshots, if there are visual adjustments introduced.
|
||||
|
||||
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
|
||||
@@ -49,6 +65,8 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
|
||||
|
||||
## 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.
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**
|
||||
|
||||
334
Cargo.lock
generated
334
Cargo.lock
generated
@@ -23,13 +23,15 @@ dependencies = [
|
||||
"language_model",
|
||||
"markdown",
|
||||
"parking_lot",
|
||||
"portable-pty",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"ui",
|
||||
@@ -37,6 +39,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"watch",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -76,7 +79,7 @@ dependencies = [
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
@@ -169,7 +172,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"ref-cast",
|
||||
"rope",
|
||||
"schemars",
|
||||
@@ -187,14 +190,15 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0-alpha.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a"
|
||||
checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -248,7 +252,6 @@ dependencies = [
|
||||
"open",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"portable-pty",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
@@ -274,9 +277,9 @@ dependencies = [
|
||||
"uuid",
|
||||
"watch",
|
||||
"web_search",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_env_vars",
|
||||
"zlog",
|
||||
"zstd",
|
||||
]
|
||||
@@ -405,7 +408,7 @@ dependencies = [
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"rules_library",
|
||||
@@ -507,7 +510,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"piper",
|
||||
"polling",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata",
|
||||
"rustix-openpty",
|
||||
"serde",
|
||||
"signal-hook",
|
||||
@@ -831,7 +834,7 @@ dependencies = [
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rpc",
|
||||
"serde",
|
||||
@@ -847,6 +850,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -929,7 +933,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -981,7 +985,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
"rust-embed",
|
||||
@@ -1382,10 +1386,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"derive_more",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"rodio",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -2286,7 +2291,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -2319,7 +2324,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2329,7 +2334,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -2456,7 +2461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2473,7 +2478,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"serde_json",
|
||||
"sum_tree",
|
||||
@@ -2894,7 +2899,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"settings",
|
||||
@@ -3081,7 +3086,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
@@ -3330,7 +3335,7 @@ dependencies = [
|
||||
"prometheus",
|
||||
"prompt_store",
|
||||
"prost 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"recent_projects",
|
||||
"release_channel",
|
||||
"remote",
|
||||
@@ -4051,6 +4056,7 @@ dependencies = [
|
||||
name = "crashes"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"crash-handler",
|
||||
"log",
|
||||
"mach2 0.5.0",
|
||||
@@ -4060,6 +4066,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"system_specs",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -4485,6 +4492,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4681,7 +4689,6 @@ dependencies = [
|
||||
"component",
|
||||
"ctor",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
@@ -4690,7 +4697,7 @@ dependencies = [
|
||||
"markdown",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -4730,7 +4737,7 @@ version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
|
||||
dependencies = [
|
||||
"nu-ansi-term 0.50.1",
|
||||
"nu-ansi-term",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5061,7 +5068,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
@@ -5556,7 +5563,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"reqwest_client",
|
||||
@@ -5629,8 +5636,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
dependencies = [
|
||||
"bit-set 0.5.3",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5640,8 +5647,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
dependencies = [
|
||||
"bit-set 0.8.0",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5718,14 +5725,10 @@ dependencies = [
|
||||
name = "feedback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"client",
|
||||
"editor",
|
||||
"gpui",
|
||||
"human_bytes",
|
||||
"menu",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"sysinfo",
|
||||
"system_specs",
|
||||
"ui",
|
||||
"urlencoding",
|
||||
"util",
|
||||
@@ -6409,7 +6412,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rope",
|
||||
"schemars",
|
||||
@@ -7295,8 +7298,8 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7462,7 +7465,7 @@ dependencies = [
|
||||
"pathfinder_geometry",
|
||||
"postage",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"raw-window-handle",
|
||||
"refineable",
|
||||
"reqwest_client",
|
||||
@@ -8301,7 +8304,7 @@ dependencies = [
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
@@ -8468,6 +8471,7 @@ dependencies = [
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"util_macros",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
@@ -8899,7 +8903,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"referencing",
|
||||
"regex",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
"reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8952,6 +8956,44 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keymap_editor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"component",
|
||||
"db",
|
||||
"editor",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"notifications",
|
||||
"paths",
|
||||
"project",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"vim",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -9036,7 +9078,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -9109,6 +9151,7 @@ dependencies = [
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars",
|
||||
@@ -9212,6 +9255,7 @@ dependencies = [
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"proto",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -9604,6 +9648,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"audio",
|
||||
"collections",
|
||||
"core-foundation 0.10.0",
|
||||
"core-video",
|
||||
@@ -9626,6 +9671,7 @@ dependencies = [
|
||||
"scap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
@@ -9698,7 +9744,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
"rustc_version",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
@@ -9770,7 +9816,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.95.1"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec"
|
||||
source = "git+https://github.com/zed-industries/lsp-types?rev=0874f8742fe55b4dc94308c1e3c0069710d8eeaf#0874f8742fe55b4dc94308c1e3c0069710d8eeaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
@@ -9913,9 +9959,11 @@ dependencies = [
|
||||
"editor",
|
||||
"fs",
|
||||
"gpui",
|
||||
"html5ever 0.27.0",
|
||||
"language",
|
||||
"linkify",
|
||||
"log",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"settings",
|
||||
@@ -9976,11 +10024,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10344,7 +10392,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"serde",
|
||||
"settings",
|
||||
@@ -10681,16 +10729,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
@@ -11189,6 +11227,8 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -11384,12 +11424,6 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.11.1"
|
||||
@@ -11612,6 +11646,12 @@ dependencies = [
|
||||
"hmac",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pciid-parser"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.5"
|
||||
@@ -12578,7 +12618,7 @@ dependencies = [
|
||||
"postage",
|
||||
"prettier",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"remote",
|
||||
@@ -13374,17 +13414,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13395,7 +13426,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13404,12 +13435,6 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
@@ -13512,6 +13537,7 @@ dependencies = [
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"telemetry_events",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -13726,7 +13752,6 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
|
||||
"serde",
|
||||
"smol",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -13867,7 +13892,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"gpui",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"smallvec",
|
||||
"sum_tree",
|
||||
@@ -13896,7 +13921,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14331,6 +14356,19 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduler"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"rand 0.9.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schema_generator"
|
||||
version = "0.1.0"
|
||||
@@ -14847,6 +14885,8 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"serde_path_to_error",
|
||||
"settings_ui_macros",
|
||||
"smallvec",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
@@ -14882,39 +14922,30 @@ name = "settings_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"command_palette",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"db",
|
||||
"debugger_ui",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"notifications",
|
||||
"paths",
|
||||
"project",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"tempfile",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"tree-sitter-json",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"vim",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "settings_ui_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15637,7 +15668,7 @@ name = "streaming_diff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ordered-float 2.10.1",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -15751,7 +15782,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"ctor",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
@@ -16130,6 +16161,21 @@ dependencies = [
|
||||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system_specs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"gpui",
|
||||
"human_bytes",
|
||||
"pciid-parser",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"sysinfo",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tab_switcher"
|
||||
version = "0.1.0"
|
||||
@@ -16327,7 +16373,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"libc",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
@@ -16375,7 +16421,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search",
|
||||
@@ -16407,7 +16453,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rope",
|
||||
"smallvec",
|
||||
@@ -16715,6 +16761,7 @@ dependencies = [
|
||||
"db",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"keymap_editor",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
@@ -16723,7 +16770,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"settings_ui",
|
||||
"smallvec",
|
||||
"story",
|
||||
"telemetry",
|
||||
@@ -17092,14 +17138,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term 0.46.0",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
@@ -17130,7 +17176,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
"serde_json",
|
||||
"streaming-iterator",
|
||||
"tree-sitter-language",
|
||||
@@ -17160,8 +17206,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter-cpp"
|
||||
version = "0.23.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -17765,7 +17810,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
@@ -17950,6 +17995,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -18556,7 +18603,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
]
|
||||
@@ -19764,7 +19811,6 @@ dependencies = [
|
||||
"any_vec",
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
"bincode",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -19783,6 +19829,7 @@ dependencies = [
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"remote",
|
||||
"schemars",
|
||||
@@ -19929,8 +19976,8 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"regalloc2",
|
||||
"regex",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"ring",
|
||||
"rust_decimal",
|
||||
"rustc-hash 1.1.0",
|
||||
@@ -20015,7 +20062,7 @@ dependencies = [
|
||||
"paths",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -20112,9 +20159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xcb"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
|
||||
checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
@@ -20371,7 +20418,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.201.8"
|
||||
version = "0.204.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
@@ -20389,6 +20436,7 @@ dependencies = [
|
||||
"auto_update",
|
||||
"auto_update_ui",
|
||||
"backtrace",
|
||||
"bincode",
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
"channel",
|
||||
@@ -20434,6 +20482,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"jj_ui",
|
||||
"journal",
|
||||
"keymap_editor",
|
||||
"language",
|
||||
"language_extension",
|
||||
"language_model",
|
||||
@@ -20487,6 +20536,7 @@ dependencies = [
|
||||
"supermaven",
|
||||
"svg_preview",
|
||||
"sysinfo",
|
||||
"system_specs",
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
@@ -20518,6 +20568,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_env_vars",
|
||||
"zeta",
|
||||
"zlog",
|
||||
"zlog_settings",
|
||||
@@ -20534,6 +20585,13 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_env_vars"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.1.0"
|
||||
@@ -20563,7 +20621,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -20597,13 +20655,6 @@ dependencies = [
|
||||
"zed_extension_api 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.2"
|
||||
@@ -20762,13 +20813,14 @@ dependencies = [
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"menu",
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
@@ -20776,6 +20828,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"theme",
|
||||
@@ -20783,7 +20836,6 @@ dependencies = [
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
|
||||
49
Cargo.toml
49
Cargo.toml
@@ -54,6 +54,8 @@ members = [
|
||||
"crates/deepseek",
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/edit_prediction",
|
||||
"crates/edit_prediction_button",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/explorer_command_injector",
|
||||
@@ -82,13 +84,12 @@ members = [
|
||||
"crates/http_client_tls",
|
||||
"crates/icons",
|
||||
"crates/image_viewer",
|
||||
"crates/edit_prediction",
|
||||
"crates/edit_prediction_button",
|
||||
"crates/inspector_ui",
|
||||
"crates/install_cli",
|
||||
"crates/jj",
|
||||
"crates/jj_ui",
|
||||
"crates/journal",
|
||||
"crates/keymap_editor",
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
@@ -130,6 +131,7 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/scheduler",
|
||||
"crates/remote",
|
||||
"crates/remote_server",
|
||||
"crates/repl",
|
||||
@@ -146,6 +148,7 @@ members = [
|
||||
"crates/settings",
|
||||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/settings_ui_macros",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
@@ -158,6 +161,7 @@ members = [
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
@@ -190,6 +194,7 @@ members = [
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta_cli",
|
||||
"crates/zlog",
|
||||
@@ -206,7 +211,6 @@ members = [
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
@@ -296,9 +300,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" }
|
||||
@@ -313,6 +315,7 @@ install_cli = { path = "crates/install_cli" }
|
||||
jj = { path = "crates/jj" }
|
||||
jj_ui = { path = "crates/jj_ui" }
|
||||
journal = { path = "crates/journal" }
|
||||
keymap_editor = { path = "crates/keymap_editor" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
@@ -357,6 +360,7 @@ 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" }
|
||||
@@ -372,6 +376,7 @@ semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_ui = { path = "crates/settings_ui" }
|
||||
settings_ui_macros = { path = "crates/settings_ui_macros" }
|
||||
snippet = { path = "crates/snippet" }
|
||||
snippet_provider = { path = "crates/snippet_provider" }
|
||||
snippets_ui = { path = "crates/snippets_ui" }
|
||||
@@ -383,6 +388,7 @@ streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
system_specs = { path = "crates/system_specs" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
@@ -416,6 +422,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" }
|
||||
@@ -424,7 +431,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = "0.1"
|
||||
agent-client-protocol = { version = "0.2.0-alpha.6", 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"
|
||||
@@ -438,6 +445,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"] }
|
||||
@@ -451,10 +459,11 @@ 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"] }
|
||||
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"
|
||||
@@ -494,6 +503,7 @@ handlebars = "4.3"
|
||||
heck = "0.5"
|
||||
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
|
||||
hex = "0.4.3"
|
||||
human_bytes = "0.4.1"
|
||||
html5ever = "0.27.0"
|
||||
http = "1.1"
|
||||
http-body = "1.0"
|
||||
@@ -515,7 +525,7 @@ libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
|
||||
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
|
||||
mach2 = "0.5"
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
@@ -530,9 +540,11 @@ objc = "0.2"
|
||||
open = "5.0.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking = "2.0"
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
parse_int = "0.9"
|
||||
pciid-parser = "0.8.0"
|
||||
pathdiff = "0.2"
|
||||
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
|
||||
@@ -551,7 +563,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"
|
||||
@@ -583,6 +595,7 @@ serde_json_lenient = { version = "0.2", features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
] }
|
||||
serde_path_to_error = "0.1.17"
|
||||
serde_repr = "0.1"
|
||||
serde_urlencoded = "0.7"
|
||||
sha2 = "0.10"
|
||||
@@ -619,7 +632,7 @@ tower-http = "0.4.4"
|
||||
tree-sitter = { version = "0.25.6", features = ["wasm"] }
|
||||
tree-sitter-bash = "0.25.0"
|
||||
tree-sitter-c = "0.23"
|
||||
tree-sitter-cpp = "0.23"
|
||||
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
|
||||
tree-sitter-css = "0.23"
|
||||
tree-sitter-diff = "0.1.0"
|
||||
tree-sitter-elixir = "0.3"
|
||||
@@ -686,6 +699,7 @@ features = [
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Hlsl",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
@@ -803,6 +817,12 @@ unexpected_cfgs = { level = "allow" }
|
||||
dbg_macro = "deny"
|
||||
todo = "deny"
|
||||
|
||||
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
|
||||
# Remove when the lint gets promoted to `suspicious`.
|
||||
declare_interior_mutable_const = "deny"
|
||||
|
||||
redundant_clone = "deny"
|
||||
|
||||
# We currently do not restrict any style rules
|
||||
# as it slows down shipping code to Zed.
|
||||
#
|
||||
@@ -831,6 +851,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",
|
||||
|
||||
2
Procfile.web
Normal file
2
Procfile.web
Normal file
@@ -0,0 +1,2 @@
|
||||
postgrest_llm: postgrest crates/collab/postgrest_llm.conf
|
||||
website: cd ../zed.dev; npm run dev -- --port=3000
|
||||
1
assets/icons/list_filter.svg
Normal file
1
assets/icons/list_filter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>
|
||||
|
After Width: | Height: | Size: 305 B |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 29 KiB |
@@ -16,7 +16,6 @@
|
||||
"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",
|
||||
@@ -41,7 +40,7 @@
|
||||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"ctrl-alt-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-alt-l": "lsp_tool::ToggleMenu"
|
||||
}
|
||||
},
|
||||
@@ -64,8 +63,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",
|
||||
@@ -121,7 +120,7 @@
|
||||
"alt-g m": "git::OpenModifiedFiles",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
@@ -131,8 +130,8 @@
|
||||
"bindings": {
|
||||
"shift-enter": "editor::Newline",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "editor::NewlineAbove",
|
||||
"ctrl-shift-enter": "editor::NewlineBelow",
|
||||
"ctrl-enter": "editor::NewlineBelow",
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
@@ -171,6 +170,7 @@
|
||||
"context": "Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-insert": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
@@ -259,6 +259,7 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-insert": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
@@ -856,7 +857,7 @@
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
@@ -1195,9 +1196,16 @@
|
||||
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||
"ctrl-3": "onboarding::ActivateAISetupPage",
|
||||
"ctrl-escape": "onboarding::Finish",
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"ctrl-enter": "onboarding::Finish",
|
||||
"alt-shift-l": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InvalidBuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
@@ -915,7 +915,7 @@
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
@@ -1301,5 +1301,12 @@
|
||||
"alt-tab": "onboarding::SignIn",
|
||||
"alt-shift-a": "onboarding::OpenAccount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "InvalidBuffer",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-enter": "workspace::OpenWithSystem"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
1260
assets/keymaps/default-windows.json
Normal file
1260
assets/keymaps/default-windows.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -38,10 +38,11 @@
|
||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,10 +38,11 @@
|
||||
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
"ctrl-x ctrl-;": "editor::ToggleComments",
|
||||
"alt-.": "editor::GoToDefinition", // xref-find-definitions
|
||||
"alt-?": "editor::FindAllReferences", // xref-find-references
|
||||
"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
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -337,7 +337,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",
|
||||
@@ -354,6 +354,15 @@
|
||||
"ctrl-s": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "showing_completions",
|
||||
"bindings": {
|
||||
"ctrl-d": "vim::ScrollDown",
|
||||
"ctrl-u": "vim::ScrollUp",
|
||||
"ctrl-e": "vim::LineDown",
|
||||
"ctrl-y": "vim::LineUp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
|
||||
"bindings": {
|
||||
@@ -428,12 +437,14 @@
|
||||
"g h": "vim::StartOfLine",
|
||||
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
|
||||
"g e": "vim::EndOfDocument",
|
||||
"g .": "vim::HelixGotoLastModification", // go to last modification
|
||||
"g r": "editor::FindAllReferences", // zed specific
|
||||
"g t": "vim::WindowTop",
|
||||
"g c": "vim::WindowMiddle",
|
||||
"g b": "vim::WindowBottom",
|
||||
|
||||
"x": "editor::SelectLine",
|
||||
"shift-r": "editor::Paste",
|
||||
"x": "vim::HelixSelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
// Window mode
|
||||
@@ -819,7 +830,7 @@
|
||||
"v": "project_panel::OpenPermanent",
|
||||
"p": "project_panel::Open",
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"s": "project_panel::OpenWithSystem",
|
||||
"s": "workspace::OpenWithSystem",
|
||||
"z d": "project_panel::CompareMarkedFiles",
|
||||
"] c": "project_panel::SelectNextGitEntry",
|
||||
"[ c": "project_panel::SelectPrevGitEntry",
|
||||
|
||||
@@ -172,7 +172,7 @@ The user has specified the following rules that should be applied:
|
||||
Rules title: {{title}}
|
||||
{{/if}}
|
||||
``````
|
||||
{{contents}}}
|
||||
{{contents}}
|
||||
``````
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
@@ -162,6 +162,12 @@
|
||||
// 2. Always quit the application
|
||||
// "on_last_window_closed": "quit_app",
|
||||
"on_last_window_closed": "platform_default",
|
||||
// Whether to show padding for zoomed panels.
|
||||
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
|
||||
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
|
||||
//
|
||||
// Default: true
|
||||
"zoomed_padding": true,
|
||||
// Whether to use the system provided dialogs for Open and Save As.
|
||||
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||
"use_system_path_prompts": true,
|
||||
@@ -182,8 +188,8 @@
|
||||
// 4. A box drawn around the following character
|
||||
// "hollow"
|
||||
//
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Default: "bar"
|
||||
"cursor_shape": "bar",
|
||||
// Determines when the mouse cursor should be hidden in an editor or input box.
|
||||
//
|
||||
// 1. Never hide the mouse cursor:
|
||||
@@ -217,9 +223,25 @@
|
||||
"current_line_highlight": "all",
|
||||
// Whether to highlight all occurrences of the selected text in an editor.
|
||||
"selection_highlight": true,
|
||||
// Whether the text selection should have rounded corners.
|
||||
"rounded_selection": true,
|
||||
// The debounce delay before querying highlights from the language
|
||||
// server based on the current cursor location.
|
||||
"lsp_highlight_debounce": 75,
|
||||
// The minimum APCA perceptual contrast between foreground and background colors.
|
||||
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
|
||||
// especially for dark mode. Values range from 0 to 106.
|
||||
//
|
||||
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
|
||||
// https://readtech.org/ARC/tests/bronze-simple-mode/
|
||||
// - 0: No contrast adjustment
|
||||
// - 45: Minimum for large fluent text (36px+)
|
||||
// - 60: Minimum for other content text
|
||||
// - 75: Minimum for body text
|
||||
// - 90: Preferred for body text
|
||||
//
|
||||
// This only affects text drawn over highlight backgrounds in the editor.
|
||||
"minimum_contrast_for_highlights": 45,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
@@ -260,8 +282,8 @@
|
||||
// - "warning"
|
||||
// - "info"
|
||||
// - "hint"
|
||||
// - null — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": null,
|
||||
// - "all" — allow all diagnostics (default)
|
||||
"diagnostics_max_severity": "all",
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
|
||||
@@ -273,6 +295,8 @@
|
||||
"redact_private_values": false,
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 5,
|
||||
// The default number of context lines shown in multibuffer excerpts.
|
||||
"excerpt_context_lines": 2,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
@@ -357,6 +381,8 @@
|
||||
// Whether to show code action buttons in the editor toolbar.
|
||||
"code_actions": false
|
||||
},
|
||||
// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only).
|
||||
"use_system_window_tabs": false,
|
||||
// Titlebar related settings
|
||||
"title_bar": {
|
||||
// Whether to show the branch icon beside branch switcher in the titlebar.
|
||||
@@ -647,6 +673,8 @@
|
||||
// "never"
|
||||
"show": "always"
|
||||
},
|
||||
// Whether to enable drag-and-drop operations in the project panel.
|
||||
"drag_and_drop": true,
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false
|
||||
},
|
||||
@@ -1133,11 +1161,6 @@
|
||||
// The minimum severity of the diagnostics to show inline.
|
||||
// Inherits editor's diagnostics' max severity settings when `null`.
|
||||
"max_severity": null
|
||||
},
|
||||
"cargo": {
|
||||
// When enabled, Zed disables rust-analyzer's check on save and starts to query
|
||||
// Cargo diagnostics separately.
|
||||
"fetch_cargo_diagnostics": false
|
||||
}
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
@@ -1503,6 +1526,11 @@
|
||||
//
|
||||
// Default: fallback
|
||||
"words": "fallback",
|
||||
// Minimum number of characters required to automatically trigger word-based completions.
|
||||
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
|
||||
//
|
||||
// Default: 3
|
||||
"words_min_length": 3,
|
||||
// Whether to fetch LSP completions or not.
|
||||
//
|
||||
// Default: true
|
||||
@@ -1629,6 +1657,9 @@
|
||||
"allowed": true
|
||||
}
|
||||
},
|
||||
"Kotlin": {
|
||||
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
|
||||
},
|
||||
"LaTeX": {
|
||||
"formatter": "language_server",
|
||||
"language_servers": ["texlab", "..."],
|
||||
@@ -1642,9 +1673,6 @@
|
||||
"use_on_type_format": false,
|
||||
"allow_rewrap": "anywhere",
|
||||
"soft_wrap": "editor_width",
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
@@ -1658,9 +1686,6 @@
|
||||
}
|
||||
},
|
||||
"Plain Text": {
|
||||
"completions": {
|
||||
"words": "disabled"
|
||||
},
|
||||
"allow_rewrap": "anywhere"
|
||||
},
|
||||
"Python": {
|
||||
@@ -1751,7 +1776,7 @@
|
||||
"api_url": "http://localhost:1234/api/v0"
|
||||
},
|
||||
"deepseek": {
|
||||
"api_url": "https://api.deepseek.com"
|
||||
"api_url": "https://api.deepseek.com/v1"
|
||||
},
|
||||
"mistral": {
|
||||
"api_url": "https://api.mistral.ai/v1"
|
||||
@@ -1899,7 +1924,10 @@
|
||||
"debugger": {
|
||||
"stepping_granularity": "line",
|
||||
"save_breakpoints": true,
|
||||
"timeout": 2000,
|
||||
"dock": "bottom",
|
||||
"log_dap_communications": true,
|
||||
"format_dap_log_messages": true,
|
||||
"button": true
|
||||
},
|
||||
// Configures any number of settings profiles that are temporarily applied on
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
// "args": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
"shell": "system"
|
||||
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
|
||||
"tags": []
|
||||
// "tags": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -31,18 +31,21 @@ language.workspace = true
|
||||
language_model.workspace = true
|
||||
markdown.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
portable-pty.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
terminal.workspace = true
|
||||
ui.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -7,6 +7,7 @@ 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};
|
||||
@@ -15,7 +16,7 @@ use settings::Settings as _;
|
||||
pub use terminal::*;
|
||||
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_client_protocol::{self as acp};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use editor::Bias;
|
||||
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
|
||||
@@ -33,7 +34,8 @@ use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
|
||||
use ui::App;
|
||||
use util::ResultExt;
|
||||
use util::{ResultExt, get_system_shell};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UserMessage {
|
||||
@@ -183,37 +185,46 @@ impl ToolCall {
|
||||
tool_call: acp::ToolCall,
|
||||
status: ToolCallStatus,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
) -> Result<Self> {
|
||||
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
|
||||
first_line.to_owned() + "…"
|
||||
} else {
|
||||
tool_call.title
|
||||
};
|
||||
Self {
|
||||
let mut content = Vec::with_capacity(tool_call.content.len());
|
||||
for item in tool_call.content {
|
||||
content.push(ToolCallContent::from_acp(
|
||||
item,
|
||||
language_registry.clone(),
|
||||
terminals,
|
||||
cx,
|
||||
)?);
|
||||
}
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.id,
|
||||
label: cx
|
||||
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
||||
kind: tool_call.kind,
|
||||
content: tool_call
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx))
|
||||
.collect(),
|
||||
content,
|
||||
locations: tool_call.locations,
|
||||
resolved_locations: Vec::default(),
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
raw_output: tool_call.raw_output,
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn update_fields(
|
||||
&mut self,
|
||||
fields: acp::ToolCallUpdateFields,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let acp::ToolCallUpdateFields {
|
||||
kind,
|
||||
status,
|
||||
@@ -248,14 +259,15 @@ impl ToolCall {
|
||||
|
||||
// Reuse existing content if we can
|
||||
for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
|
||||
old.update_from_acp(new, language_registry.clone(), cx);
|
||||
old.update_from_acp(new, language_registry.clone(), terminals, cx)?;
|
||||
}
|
||||
for new in content {
|
||||
self.content.push(ToolCallContent::from_acp(
|
||||
new,
|
||||
language_registry.clone(),
|
||||
terminals,
|
||||
cx,
|
||||
))
|
||||
)?)
|
||||
}
|
||||
self.content.truncate(new_content_len);
|
||||
}
|
||||
@@ -279,6 +291,7 @@ impl ToolCall {
|
||||
}
|
||||
self.raw_output = Some(raw_output);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
|
||||
@@ -549,13 +562,16 @@ impl ToolCallContent {
|
||||
pub fn from_acp(
|
||||
content: acp::ToolCallContent,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
) -> Result<Self> {
|
||||
match content {
|
||||
acp::ToolCallContent::Content { content } => {
|
||||
Self::ContentBlock(ContentBlock::new(content, &language_registry, cx))
|
||||
}
|
||||
acp::ToolCallContent::Diff { diff } => Self::Diff(cx.new(|cx| {
|
||||
acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
|
||||
content,
|
||||
&language_registry,
|
||||
cx,
|
||||
))),
|
||||
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
|
||||
Diff::finalized(
|
||||
diff.path,
|
||||
diff.old_text,
|
||||
@@ -563,7 +579,12 @@ impl ToolCallContent {
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})),
|
||||
}))),
|
||||
acp::ToolCallContent::Terminal { terminal_id } => terminals
|
||||
.get(&terminal_id)
|
||||
.cloned()
|
||||
.map(Self::Terminal)
|
||||
.ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,8 +592,9 @@ impl ToolCallContent {
|
||||
&mut self,
|
||||
new: acp::ToolCallContent,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let needs_update = match (&self, &new) {
|
||||
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
|
||||
old_diff.read(cx).needs_update(
|
||||
@@ -585,8 +607,9 @@ impl ToolCallContent {
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
*self = Self::from_acp(new, language_registry, cx);
|
||||
*self = Self::from_acp(new, language_registry, terminals, cx)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
@@ -763,6 +786,8 @@ pub struct AcpThread {
|
||||
token_usage: Option<TokenUsage>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
|
||||
determine_shell: Shared<Task<String>>,
|
||||
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -778,6 +803,8 @@ pub enum AcpThreadEvent {
|
||||
Error,
|
||||
LoadError(LoadError),
|
||||
PromptCapabilitiesUpdated,
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -846,6 +873,20 @@ 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(),
|
||||
@@ -859,6 +900,8 @@ impl AcpThread {
|
||||
token_usage: None,
|
||||
prompt_capabilities,
|
||||
_observe_prompt_capabilities: task,
|
||||
terminals: HashMap::default(),
|
||||
determine_shell,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,6 +1004,9 @@ impl AcpThread {
|
||||
acp::SessionUpdate::Plan(plan) => {
|
||||
self.update_plan(plan, cx);
|
||||
}
|
||||
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
|
||||
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1082,27 +1128,28 @@ impl AcpThread {
|
||||
let update = update.into();
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
|
||||
let (ix, current_call) = self
|
||||
.tool_call_mut(update.id())
|
||||
let ix = self
|
||||
.index_for_tool_call(update.id())
|
||||
.context("Tool call not found")?;
|
||||
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
match update {
|
||||
ToolCallUpdate::UpdateFields(update) => {
|
||||
let location_updated = update.fields.locations.is_some();
|
||||
current_call.update_fields(update.fields, languages, cx);
|
||||
call.update_fields(update.fields, languages, &self.terminals, cx)?;
|
||||
if location_updated {
|
||||
self.resolve_locations(update.id, cx);
|
||||
}
|
||||
}
|
||||
ToolCallUpdate::UpdateDiff(update) => {
|
||||
current_call.content.clear();
|
||||
current_call
|
||||
.content
|
||||
.push(ToolCallContent::Diff(update.diff));
|
||||
call.content.clear();
|
||||
call.content.push(ToolCallContent::Diff(update.diff));
|
||||
}
|
||||
ToolCallUpdate::UpdateTerminal(update) => {
|
||||
current_call.content.clear();
|
||||
current_call
|
||||
.content
|
||||
call.content.clear();
|
||||
call.content
|
||||
.push(ToolCallContent::Terminal(update.terminal));
|
||||
}
|
||||
}
|
||||
@@ -1125,21 +1172,30 @@ impl AcpThread {
|
||||
/// Fails if id does not match an existing entry.
|
||||
pub fn upsert_tool_call_inner(
|
||||
&mut self,
|
||||
tool_call_update: acp::ToolCallUpdate,
|
||||
update: acp::ToolCallUpdate,
|
||||
status: ToolCallStatus,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<(), acp::Error> {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let id = tool_call_update.id.clone();
|
||||
let id = update.id.clone();
|
||||
|
||||
if let Some((ix, current_call)) = self.tool_call_mut(&id) {
|
||||
current_call.update_fields(tool_call_update.fields, language_registry, cx);
|
||||
current_call.status = status;
|
||||
if let Some(ix) = self.index_for_tool_call(&id) {
|
||||
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
call.update_fields(update.fields, language_registry, &self.terminals, cx)?;
|
||||
call.status = status;
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
} else {
|
||||
let call =
|
||||
ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx);
|
||||
let call = ToolCall::from_acp(
|
||||
update.try_into()?,
|
||||
status,
|
||||
language_registry,
|
||||
&self.terminals,
|
||||
cx,
|
||||
)?;
|
||||
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
|
||||
};
|
||||
|
||||
@@ -1147,6 +1203,22 @@ impl AcpThread {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option<usize> {
|
||||
self.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find_map(|(index, entry)| {
|
||||
if let AgentThreadEntry::ToolCall(tool_call) = entry
|
||||
&& &tool_call.id == id
|
||||
{
|
||||
Some(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
|
||||
// The tool call we are looking for is typically the last one, or very close to the end.
|
||||
// At the moment, it doesn't seem like a hashmap would be a good fit for this use case.
|
||||
@@ -1495,15 +1567,42 @@ 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,
|
||||
})) = 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);
|
||||
@@ -1829,6 +1928,133 @@ impl AcpThread {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_terminal(
|
||||
&self,
|
||||
mut 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)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
|
||||
let env = cx.spawn(async move |_, _| {
|
||||
let mut env = env.await.unwrap_or_default();
|
||||
if cfg!(unix) {
|
||||
env.insert("PAGER".into(), "cat".into());
|
||||
}
|
||||
for var in extra_env {
|
||||
env.insert(var.name, var.value);
|
||||
}
|
||||
env
|
||||
});
|
||||
|
||||
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 terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_task(
|
||||
task::SpawnInTerminal {
|
||||
command: Some(program),
|
||||
args,
|
||||
cwd: cwd.clone(),
|
||||
env,
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
cx.new(|cx| {
|
||||
Terminal::new(
|
||||
terminal_id,
|
||||
command,
|
||||
cwd,
|
||||
output_byte_limit.map(|l| l as usize),
|
||||
terminal,
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let terminal = terminal_task.await?;
|
||||
this.update(cx, |this, _cx| {
|
||||
this.terminals.insert(terminal_id, terminal.clone());
|
||||
terminal
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn kill_terminal(
|
||||
&mut self,
|
||||
terminal_id: acp::TerminalId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
self.terminals
|
||||
.get(&terminal_id)
|
||||
.context("Terminal not found")?
|
||||
.update(cx, |terminal, cx| {
|
||||
terminal.kill(cx);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn release_terminal(
|
||||
&mut self,
|
||||
terminal_id: acp::TerminalId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
self.terminals
|
||||
.remove(&terminal_id)
|
||||
.context("Terminal not found")?
|
||||
.update(cx, |terminal, cx| {
|
||||
terminal.kill(cx);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn terminal(&self, terminal_id: acp::TerminalId) -> Result<Entity<Terminal>> {
|
||||
self.terminals
|
||||
.get(&terminal_id)
|
||||
.context("Terminal not found")
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
self.entries.iter().map(|e| e.to_markdown(cx)).collect()
|
||||
}
|
||||
@@ -1888,7 +2114,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 _;
|
||||
@@ -2480,6 +2706,187 @@ 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"}),
|
||||
),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
// Now return refusal because of the tool result
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
})
|
||||
} else {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
}));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new("/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,
|
||||
})],
|
||||
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,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
} else {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
})
|
||||
}
|
||||
.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);
|
||||
@@ -2543,8 +2950,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
|
||||
@@ -2650,8 +3057,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>()
|
||||
|
||||
@@ -75,7 +75,6 @@ pub trait AgentConnection {
|
||||
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
@@ -232,13 +231,6 @@ impl AgentModelList {
|
||||
AgentModelList::Grouped(groups) => groups.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
match self {
|
||||
AgentModelList::Flat(models) => models.len(),
|
||||
AgentModelList::Grouped(groups) => groups.values().len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{MultiBuffer, PathKey};
|
||||
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
@@ -64,7 +64,7 @@ impl Diff {
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(diff, cx);
|
||||
@@ -279,7 +279,7 @@ impl PendingDiff {
|
||||
path_key,
|
||||
buffer,
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff.clone(), cx);
|
||||
@@ -305,7 +305,7 @@ impl PendingDiff {
|
||||
PathKey::for_buffer(&self.new_buffer, cx),
|
||||
self.new_buffer.clone(),
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
let end = multibuffer.len(cx);
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
use gpui::{App, AppContext, Context, Entity};
|
||||
use agent_client_protocol as acp;
|
||||
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{App, AppContext, Context, Entity, Task};
|
||||
use language::LanguageRegistry;
|
||||
use markdown::Markdown;
|
||||
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
|
||||
|
||||
pub struct Terminal {
|
||||
id: acp::TerminalId,
|
||||
command: Entity<Markdown>,
|
||||
working_dir: Option<PathBuf>,
|
||||
terminal: Entity<terminal::Terminal>,
|
||||
started_at: Instant,
|
||||
output: Option<TerminalOutput>,
|
||||
output_byte_limit: Option<usize>,
|
||||
_output_task: Shared<Task<acp::TerminalExitStatus>>,
|
||||
}
|
||||
|
||||
pub struct TerminalOutput {
|
||||
pub ended_at: Instant,
|
||||
pub exit_status: Option<ExitStatus>,
|
||||
pub was_content_truncated: bool,
|
||||
pub content: String,
|
||||
pub original_content_len: usize,
|
||||
pub content_line_count: usize,
|
||||
pub finished_with_empty_output: bool,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn new(
|
||||
id: acp::TerminalId,
|
||||
command: String,
|
||||
working_dir: Option<PathBuf>,
|
||||
output_byte_limit: Option<usize>,
|
||||
terminal: Entity<terminal::Terminal>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let command_task = terminal.read(cx).wait_for_completed_task(cx);
|
||||
Self {
|
||||
id,
|
||||
command: cx.new(|cx| {
|
||||
Markdown::new(
|
||||
format!("```\n{}\n```", command).into(),
|
||||
@@ -41,27 +50,93 @@ impl Terminal {
|
||||
terminal,
|
||||
started_at: Instant::now(),
|
||||
output: None,
|
||||
output_byte_limit,
|
||||
_output_task: cx
|
||||
.spawn(async move |this, cx| {
|
||||
let exit_status = command_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let (content, original_content_len) = this.truncated_output(cx);
|
||||
let content_line_count = this.terminal.read(cx).total_lines();
|
||||
|
||||
this.output = Some(TerminalOutput {
|
||||
ended_at: Instant::now(),
|
||||
exit_status,
|
||||
content,
|
||||
original_content_len,
|
||||
content_line_count,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
|
||||
|
||||
acp::TerminalExitStatus {
|
||||
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
|
||||
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
|
||||
}
|
||||
})
|
||||
.shared(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(
|
||||
&mut self,
|
||||
exit_status: Option<ExitStatus>,
|
||||
original_content_len: usize,
|
||||
truncated_content_len: usize,
|
||||
content_line_count: usize,
|
||||
finished_with_empty_output: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.output = Some(TerminalOutput {
|
||||
ended_at: Instant::now(),
|
||||
exit_status,
|
||||
was_content_truncated: truncated_content_len < original_content_len,
|
||||
original_content_len,
|
||||
content_line_count,
|
||||
finished_with_empty_output,
|
||||
pub fn id(&self) -> &acp::TerminalId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
|
||||
self._output_task.clone()
|
||||
}
|
||||
|
||||
pub fn kill(&mut self, cx: &mut App) {
|
||||
self.terminal.update(cx, |terminal, _cx| {
|
||||
terminal.kill_active_task();
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
|
||||
|
||||
acp::TerminalOutputResponse {
|
||||
output: output.content.clone(),
|
||||
truncated: output.original_content_len > output.content.len(),
|
||||
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)),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
let (current_content, original_len) = self.truncated_output(cx);
|
||||
|
||||
acp::TerminalOutputResponse {
|
||||
truncated: current_content.len() < original_len,
|
||||
output: current_content,
|
||||
exit_status: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn truncated_output(&self, cx: &App) -> (String, usize) {
|
||||
let terminal = self.terminal.read(cx);
|
||||
let mut content = terminal.get_content();
|
||||
|
||||
let original_content_len = content.len();
|
||||
|
||||
if let Some(limit) = self.output_byte_limit
|
||||
&& content.len() > limit
|
||||
{
|
||||
let mut end_ix = limit.min(content.len());
|
||||
while !content.is_char_boundary(end_ix) {
|
||||
end_ix -= 1;
|
||||
}
|
||||
// Don't truncate mid-line, clear the remainder of the last line
|
||||
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
|
||||
content.truncate(end_ix);
|
||||
}
|
||||
|
||||
(content, original_content_len)
|
||||
}
|
||||
|
||||
pub fn command(&self) -> &Entity<Markdown> {
|
||||
|
||||
@@ -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,11 +1,10 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
|
||||
use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use extension_host::{ExtensionOperation, ExtensionStore};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
|
||||
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
|
||||
Styled, Transformation, Window, actions, percentage,
|
||||
App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Window, actions,
|
||||
};
|
||||
use language::{
|
||||
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
|
||||
@@ -25,7 +24,10 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::truncate_and_trailoff;
|
||||
use workspace::{StatusItemView, Workspace, item::ItemHandle};
|
||||
|
||||
@@ -82,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| {
|
||||
@@ -100,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| {
|
||||
@@ -405,13 +392,7 @@ impl ActivityIndicator {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
),
|
||||
message,
|
||||
@@ -433,11 +414,7 @@ impl ActivityIndicator {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Debug: {}", session.read(cx).adapter()),
|
||||
@@ -460,11 +437,7 @@ impl ActivityIndicator {
|
||||
icon: Some(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: job_info.message.into(),
|
||||
@@ -671,8 +644,9 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
self.auto_updater
|
||||
.as_ref()
|
||||
.and_then(|updater| match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
@@ -728,28 +702,49 @@ impl ActivityIndicator {
|
||||
tooltip_message: None,
|
||||
}),
|
||||
AutoUpdateStatus::Idle => None,
|
||||
};
|
||||
}
|
||||
})
|
||||
.or_else(|| {
|
||||
if let Some(extension_store) =
|
||||
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
|
||||
&& let Some((extension_id, operation)) =
|
||||
extension_store.outstanding_operations().iter().next()
|
||||
{
|
||||
let (message, icon, rotate) = match operation {
|
||||
ExtensionOperation::Install => (
|
||||
format!("Installing {extension_id} extension…"),
|
||||
IconName::LoadCircle,
|
||||
true,
|
||||
),
|
||||
ExtensionOperation::Upgrade => (
|
||||
format!("Updating {extension_id} extension…"),
|
||||
IconName::Download,
|
||||
false,
|
||||
),
|
||||
ExtensionOperation::Remove => (
|
||||
format!("Removing {extension_id} extension…"),
|
||||
IconName::LoadCircle,
|
||||
true,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(extension_store) =
|
||||
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
|
||||
&& let Some(extension_id) = extension_store.outstanding_operations().keys().next()
|
||||
{
|
||||
return Some(Content {
|
||||
icon: Some(
|
||||
Icon::new(IconName::Download)
|
||||
.size(IconSize::Small)
|
||||
.into_any_element(),
|
||||
),
|
||||
message: format!("Updating {extension_id} extension…"),
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&DismissErrorMessage, window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
Some(Content {
|
||||
icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
|
||||
if rotate {
|
||||
this.with_rotate_animation(3).into_any_element()
|
||||
} else {
|
||||
this.into_any_element()
|
||||
}
|
||||
})),
|
||||
message,
|
||||
on_click: Some(Arc::new(|this, window, cx| {
|
||||
this.dismiss_error_message(&Default::default(), window, cx)
|
||||
})),
|
||||
tooltip_message: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn version_tooltip_message(version: &VersionCheckType) -> String {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -48,7 +48,6 @@ log.workspace = true
|
||||
open.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
portable-pty.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
rust-embed.workspace = true
|
||||
@@ -68,8 +67,8 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
web_search.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
|
||||
UserMessageContent, templates::Templates,
|
||||
};
|
||||
use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated};
|
||||
use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated};
|
||||
use acp_thread::{AcpThread, AgentModelSelector};
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
@@ -10,7 +10,8 @@ use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::channel::mpsc;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::future::Shared;
|
||||
use futures::{StreamExt, future};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
|
||||
@@ -23,7 +24,7 @@ use prompt_store::{
|
||||
use settings::update_settings_file;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
@@ -276,13 +277,6 @@ impl NativeAgent {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<AcpThread> {
|
||||
let connection = Rc::new(NativeAgentConnection(cx.entity()));
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||
|
||||
thread_handle.update(cx, |thread, cx| {
|
||||
thread.set_summarization_model(summarization_model, cx);
|
||||
thread.add_default_tools(cx)
|
||||
});
|
||||
|
||||
let thread = thread_handle.read(cx);
|
||||
let session_id = thread.id().clone();
|
||||
@@ -301,6 +295,20 @@ impl NativeAgent {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||
|
||||
thread_handle.update(cx, |thread, cx| {
|
||||
thread.set_summarization_model(summarization_model, cx);
|
||||
thread.add_default_tools(
|
||||
Rc::new(AcpThreadEnvironment {
|
||||
acp_thread: acp_thread.downgrade(),
|
||||
}) as _,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
|
||||
this.sessions.remove(acp_thread.session_id());
|
||||
@@ -1001,7 +1009,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
|
||||
self.0.read_with(cx, |agent, _cx| {
|
||||
agent.sessions.get(session_id).map(|session| {
|
||||
Rc::new(NativeAgentSessionEditor {
|
||||
Rc::new(NativeAgentSessionTruncate {
|
||||
thread: session.thread.clone(),
|
||||
acp_thread: session.acp_thread.clone(),
|
||||
}) as _
|
||||
@@ -1050,12 +1058,12 @@ impl acp_thread::AgentTelemetry for NativeAgentConnection {
|
||||
}
|
||||
}
|
||||
|
||||
struct NativeAgentSessionEditor {
|
||||
struct NativeAgentSessionTruncate {
|
||||
thread: Entity<Thread>,
|
||||
acp_thread: WeakEntity<AcpThread>,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor {
|
||||
impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate {
|
||||
fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
|
||||
match self.thread.update(cx, |thread, cx| {
|
||||
thread.truncate(message_id.clone(), cx)?;
|
||||
@@ -1104,6 +1112,66 @@ impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpThreadEnvironment {
|
||||
acp_thread: WeakEntity<AcpThread>,
|
||||
}
|
||||
|
||||
impl ThreadEnvironment for AcpThreadEnvironment {
|
||||
fn create_terminal(
|
||||
&self,
|
||||
command: String,
|
||||
cwd: Option<PathBuf>,
|
||||
output_byte_limit: Option<u64>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Rc<dyn TerminalHandle>>> {
|
||||
let task = self.acp_thread.update(cx, |thread, cx| {
|
||||
thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx)
|
||||
});
|
||||
|
||||
let acp_thread = self.acp_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let terminal = task?.await?;
|
||||
|
||||
let (drop_tx, drop_rx) = oneshot::channel();
|
||||
let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
drop_rx.await.ok();
|
||||
acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx))
|
||||
})
|
||||
.detach();
|
||||
|
||||
let handle = AcpTerminalHandle {
|
||||
terminal,
|
||||
_drop_tx: Some(drop_tx),
|
||||
};
|
||||
|
||||
Ok(Rc::new(handle) as _)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpTerminalHandle {
|
||||
terminal: Entity<acp_thread::Terminal>,
|
||||
_drop_tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl TerminalHandle for AcpTerminalHandle {
|
||||
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
|
||||
self.terminal.read_with(cx, |term, _cx| term.id().clone())
|
||||
}
|
||||
|
||||
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
|
||||
self.terminal
|
||||
.read_with(cx, |term, _cx| term.wait_for_exit())
|
||||
}
|
||||
|
||||
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
|
||||
self.terminal
|
||||
.read_with(cx, |term, cx| term.current_output(cx))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::HistoryEntryId;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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();
|
||||
@@ -1349,7 +1348,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 +1686,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();
|
||||
@@ -2353,15 +2350,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 +2477,7 @@ fn setup_context_server(
|
||||
path: "somebinary".into(),
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
timeout: None,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -45,14 +45,15 @@ use schemars::{JsonSchema, Schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use smol::stream::StreamExt;
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::RangeInclusive,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -523,6 +524,22 @@ pub enum AgentMessageContent {
|
||||
ToolUse(LanguageModelToolUse),
|
||||
}
|
||||
|
||||
pub trait TerminalHandle {
|
||||
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
|
||||
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
|
||||
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
|
||||
}
|
||||
|
||||
pub trait ThreadEnvironment {
|
||||
fn create_terminal(
|
||||
&self,
|
||||
command: String,
|
||||
cwd: Option<PathBuf>,
|
||||
output_byte_limit: Option<u64>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Rc<dyn TerminalHandle>>>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThreadEvent {
|
||||
UserMessage(UserMessage),
|
||||
@@ -535,6 +552,14 @@ pub enum ThreadEvent {
|
||||
Stop(acp::StopReason),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NewTerminal {
|
||||
pub command: String,
|
||||
pub output_byte_limit: Option<u64>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolCallAuthorization {
|
||||
pub tool_call: acp::ToolCallUpdate,
|
||||
@@ -1024,7 +1049,11 @@ impl Thread {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_default_tools(&mut self, cx: &mut Context<Self>) {
|
||||
pub fn add_default_tools(
|
||||
&mut self,
|
||||
environment: Rc<dyn ThreadEnvironment>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
self.add_tool(CopyPathTool::new(self.project.clone()));
|
||||
self.add_tool(CreateDirectoryTool::new(self.project.clone()));
|
||||
@@ -1045,7 +1074,7 @@ impl Thread {
|
||||
self.project.clone(),
|
||||
self.action_log.clone(),
|
||||
));
|
||||
self.add_tool(TerminalTool::new(self.project.clone(), cx));
|
||||
self.add_tool(TerminalTool::new(self.project.clone(), environment));
|
||||
self.add_tool(ThinkingTool);
|
||||
self.add_tool(WebSearchTool);
|
||||
}
|
||||
@@ -2389,19 +2418,6 @@ impl ToolCallEventStream {
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn update_terminal(&self, terminal: Entity<acp_thread::Terminal>) {
|
||||
self.stream
|
||||
.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
|
||||
acp_thread::ToolCallUpdateTerminal {
|
||||
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
|
||||
terminal,
|
||||
}
|
||||
.into(),
|
||||
)))
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub fn authorize(&self, title: impl Into<String>, cx: &mut App) -> Task<Result<()>> {
|
||||
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
|
||||
return Task::ready(Ok(()));
|
||||
|
||||
@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[schemars(inline)]
|
||||
pub enum EditFileMode {
|
||||
Edit,
|
||||
Create,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::Result;
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{App, AppContext, Entity, SharedString, Task};
|
||||
use project::{Project, terminals::TerminalKind};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
|
||||
use util::markdown::MarkdownInlineCode;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream};
|
||||
|
||||
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
|
||||
const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024;
|
||||
|
||||
/// Executes a shell one-liner and returns the combined output.
|
||||
///
|
||||
@@ -36,25 +36,14 @@ pub struct TerminalToolInput {
|
||||
|
||||
pub struct TerminalTool {
|
||||
project: Entity<Project>,
|
||||
determine_shell: Shared<Task<String>>,
|
||||
environment: Rc<dyn ThreadEnvironment>,
|
||||
}
|
||||
|
||||
impl TerminalTool {
|
||||
pub fn new(project: Entity<Project>, cx: &mut App) -> Self {
|
||||
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()
|
||||
}
|
||||
});
|
||||
pub fn new(project: Entity<Project>, environment: Rc<dyn ThreadEnvironment>) -> Self {
|
||||
Self {
|
||||
project,
|
||||
determine_shell: determine_shell.shared(),
|
||||
environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,128 +88,49 @@ impl AgentTool for TerminalTool {
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let working_dir = match working_dir(&input, &self.project, cx) {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
let program = self.determine_shell.clone();
|
||||
let command = if cfg!(windows) {
|
||||
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
|
||||
} else if let Some(cwd) = working_dir
|
||||
.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", input.command)
|
||||
} else {
|
||||
format!("({}) </dev/null", input.command)
|
||||
};
|
||||
let args = vec!["-c".into(), command];
|
||||
|
||||
let env = match &working_dir {
|
||||
Some(dir) => self.project.update(cx, |project, cx| {
|
||||
project.directory_environment(dir.as_path().into(), cx)
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
|
||||
let env = cx.spawn(async move |_| {
|
||||
let mut env = env.await.unwrap_or_default();
|
||||
if cfg!(unix) {
|
||||
env.insert("PAGER".into(), "cat".into());
|
||||
}
|
||||
env
|
||||
});
|
||||
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
|
||||
cx.spawn(async move |cx| {
|
||||
authorize.await?;
|
||||
|
||||
cx.spawn({
|
||||
async move |cx| {
|
||||
authorize.await?;
|
||||
let terminal = self
|
||||
.environment
|
||||
.create_terminal(
|
||||
input.command.clone(),
|
||||
working_dir,
|
||||
Some(COMMAND_OUTPUT_LIMIT),
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let program = program.await;
|
||||
let env = env.await;
|
||||
let terminal = self
|
||||
.project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal(
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
command: Some(program),
|
||||
args,
|
||||
cwd: working_dir.clone(),
|
||||
env,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let acp_terminal = cx.new(|cx| {
|
||||
acp_thread::Terminal::new(
|
||||
input.command.clone(),
|
||||
working_dir.clone(),
|
||||
terminal.clone(),
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
event_stream.update_terminal(acp_terminal.clone());
|
||||
let terminal_id = terminal.id(cx)?;
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let exit_status = terminal
|
||||
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.await;
|
||||
let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
|
||||
(terminal.get_content(), terminal.total_lines())
|
||||
})?;
|
||||
let exit_status = terminal.wait_for_exit(cx)?.await;
|
||||
let output = terminal.current_output(cx)?;
|
||||
|
||||
let (processed_content, finished_with_empty_output) = process_content(
|
||||
&content,
|
||||
&input.command,
|
||||
exit_status.map(portable_pty::ExitStatus::from),
|
||||
);
|
||||
|
||||
acp_terminal
|
||||
.update(cx, |terminal, cx| {
|
||||
terminal.finish(
|
||||
exit_status,
|
||||
content.len(),
|
||||
processed_content.len(),
|
||||
content_line_count,
|
||||
finished_with_empty_output,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(processed_content)
|
||||
}
|
||||
Ok(process_content(output, &input.command, exit_status))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn process_content(
|
||||
content: &str,
|
||||
output: acp::TerminalOutputResponse,
|
||||
command: &str,
|
||||
exit_status: Option<portable_pty::ExitStatus>,
|
||||
) -> (String, bool) {
|
||||
let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
|
||||
|
||||
let content = if should_truncate {
|
||||
let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len());
|
||||
while !content.is_char_boundary(end_ix) {
|
||||
end_ix -= 1;
|
||||
}
|
||||
// Don't truncate mid-line, clear the remainder of the last line
|
||||
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
|
||||
&content[..end_ix]
|
||||
} else {
|
||||
content
|
||||
};
|
||||
let content = content.trim();
|
||||
exit_status: acp::TerminalExitStatus,
|
||||
) -> String {
|
||||
let content = output.output.trim();
|
||||
let is_empty = content.is_empty();
|
||||
|
||||
let content = format!("```\n{content}\n```");
|
||||
let content = if should_truncate {
|
||||
let content = if output.truncated {
|
||||
format!(
|
||||
"Command output too long. The first {} bytes:\n\n{content}",
|
||||
content.len(),
|
||||
@@ -229,24 +139,21 @@ fn process_content(
|
||||
content
|
||||
};
|
||||
|
||||
let content = match exit_status {
|
||||
Some(exit_status) if exit_status.success() => {
|
||||
let content = match exit_status.exit_code {
|
||||
Some(0) => {
|
||||
if is_empty {
|
||||
"Command executed successfully.".to_string()
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
Some(exit_status) => {
|
||||
Some(exit_code) => {
|
||||
if is_empty {
|
||||
format!(
|
||||
"Command \"{command}\" failed with exit code {}.",
|
||||
exit_status.exit_code()
|
||||
)
|
||||
format!("Command \"{command}\" failed with exit code {}.", exit_code)
|
||||
} else {
|
||||
format!(
|
||||
"Command \"{command}\" failed with exit code {}.\n\n{content}",
|
||||
exit_status.exit_code()
|
||||
exit_code
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -257,7 +164,7 @@ fn process_content(
|
||||
)
|
||||
}
|
||||
};
|
||||
(content, is_empty)
|
||||
content
|
||||
}
|
||||
|
||||
fn working_dir(
|
||||
@@ -300,169 +207,3 @@ fn working_dir(
|
||||
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use agent_settings::AgentSettings;
|
||||
use editor::EditorSettings;
|
||||
use fs::RealFs;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use terminal::terminal_settings::TerminalSettings;
|
||||
use theme::ThemeSettings;
|
||||
use util::test::TempTree;
|
||||
|
||||
use crate::ThreadEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
zlog::init_test();
|
||||
|
||||
executor.allow_parking();
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
ThemeSettings::register(cx);
|
||||
TerminalSettings::register(cx);
|
||||
EditorSettings::register(cx);
|
||||
AgentSettings::register(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
if cfg!(windows) {
|
||||
return;
|
||||
}
|
||||
|
||||
init_test(&executor, cx);
|
||||
|
||||
let fs = Arc::new(RealFs::new(None, executor));
|
||||
let tree = TempTree::new(json!({
|
||||
"project": {},
|
||||
}));
|
||||
let project: Entity<Project> =
|
||||
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
|
||||
|
||||
let input = TerminalToolInput {
|
||||
command: "cat".to_owned(),
|
||||
cd: tree
|
||||
.path()
|
||||
.join("project")
|
||||
.as_path()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
};
|
||||
let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test();
|
||||
let result = cx
|
||||
.update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx));
|
||||
|
||||
let auth = event_stream_rx.expect_authorization().await;
|
||||
auth.response.send(auth.options[0].id.clone()).unwrap();
|
||||
event_stream_rx.expect_terminal().await;
|
||||
assert_eq!(result.await.unwrap(), "Command executed successfully.");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
if cfg!(windows) {
|
||||
return;
|
||||
}
|
||||
|
||||
init_test(&executor, cx);
|
||||
|
||||
let fs = Arc::new(RealFs::new(None, executor));
|
||||
let tree = TempTree::new(json!({
|
||||
"project": {},
|
||||
"other-project": {},
|
||||
}));
|
||||
let project: Entity<Project> =
|
||||
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
|
||||
|
||||
let check = |input, expected, cx: &mut TestAppContext| {
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let result = cx.update(|cx| {
|
||||
Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let event = stream_rx.try_next();
|
||||
if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event {
|
||||
auth.response.send(auth.options[0].id.clone()).unwrap();
|
||||
}
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
let output = result.await;
|
||||
assert_eq!(output.ok(), expected);
|
||||
})
|
||||
};
|
||||
|
||||
check(
|
||||
TerminalToolInput {
|
||||
command: "pwd".into(),
|
||||
cd: ".".into(),
|
||||
},
|
||||
Some(format!(
|
||||
"```\n{}\n```",
|
||||
tree.path().join("project").display()
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
check(
|
||||
TerminalToolInput {
|
||||
command: "pwd".into(),
|
||||
cd: "other-project".into(),
|
||||
},
|
||||
None, // other-project is a dir, but *not* a worktree (yet)
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Absolute path above the worktree root
|
||||
check(
|
||||
TerminalToolInput {
|
||||
command: "pwd".into(),
|
||||
cd: tree.path().to_string_lossy().into(),
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_worktree(tree.path().join("other-project"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
check(
|
||||
TerminalToolInput {
|
||||
command: "pwd".into(),
|
||||
cd: "other-project".into(),
|
||||
},
|
||||
Some(format!(
|
||||
"```\n{}\n```",
|
||||
tree.path().join("other-project").display()
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
check(
|
||||
TerminalToolInput {
|
||||
command: "pwd".into(),
|
||||
cd: ".".into(),
|
||||
},
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ pub struct AcpConnection {
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
prompt_capabilities: acp::PromptCapabilities,
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
_io_task: Task<Result<()>>,
|
||||
_wait_task: Task<Result<()>>,
|
||||
_stderr_task: Task<Result<()>>,
|
||||
@@ -134,6 +134,7 @@ impl AcpConnection {
|
||||
read_text_file: true,
|
||||
write_text_file: true,
|
||||
},
|
||||
terminal: true,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
@@ -147,7 +148,7 @@ impl AcpConnection {
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
|
||||
agent_capabilities: response.agent_capabilities,
|
||||
_io_task: io_task,
|
||||
_wait_task: wait_task,
|
||||
_stderr_task: stderr_task,
|
||||
@@ -155,7 +156,7 @@ impl AcpConnection {
|
||||
}
|
||||
|
||||
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
|
||||
&self.prompt_capabilities
|
||||
&self.agent_capabilities.prompt_capabilities
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +223,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.prompt_capabilities),
|
||||
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
@@ -344,11 +345,7 @@ impl acp::Client for ClientDelegate {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.session_thread(&arguments.session_id)?
|
||||
.update(cx, |thread, cx| {
|
||||
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
|
||||
})??;
|
||||
@@ -364,11 +361,7 @@ impl acp::Client for ClientDelegate {
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.session_thread(&arguments.session_id)?
|
||||
.update(cx, |thread, cx| {
|
||||
thread.write_text_file(arguments.path, arguments.content, cx)
|
||||
})?;
|
||||
@@ -382,16 +375,12 @@ impl acp::Client for ClientDelegate {
|
||||
&self,
|
||||
arguments: acp::ReadTextFileRequest,
|
||||
) -> Result<acp::ReadTextFileResponse, acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let task = self
|
||||
.sessions
|
||||
.borrow()
|
||||
.get(&arguments.session_id)
|
||||
.context("Failed to get session")?
|
||||
.thread
|
||||
.update(cx, |thread, cx| {
|
||||
let task = self.session_thread(&arguments.session_id)?.update(
|
||||
&mut self.cx.clone(),
|
||||
|thread, cx| {
|
||||
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
@@ -402,16 +391,92 @@ impl acp::Client for ClientDelegate {
|
||||
&self,
|
||||
notification: acp::SessionNotification,
|
||||
) -> Result<(), acp::Error> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions
|
||||
.get(¬ification.session_id)
|
||||
.context("Failed to get session")?;
|
||||
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
self.session_thread(¬ification.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.handle_session_update(notification.update, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_terminal(
|
||||
&self,
|
||||
args: acp::CreateTerminalRequest,
|
||||
) -> Result<acp::CreateTerminalResponse, acp::Error> {
|
||||
let terminal = self
|
||||
.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.create_terminal(
|
||||
args.command,
|
||||
args.args,
|
||||
args.env,
|
||||
args.cwd,
|
||||
args.output_byte_limit,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
Ok(
|
||||
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
|
||||
terminal_id: terminal.id().clone(),
|
||||
})?,
|
||||
)
|
||||
}
|
||||
|
||||
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.kill_terminal(args.terminal_id, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.release_terminal(args.terminal_id, cx)
|
||||
})??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn terminal_output(
|
||||
&self,
|
||||
args: acp::TerminalOutputRequest,
|
||||
) -> Result<acp::TerminalOutputResponse, acp::Error> {
|
||||
self.session_thread(&args.session_id)?
|
||||
.read_with(&mut self.cx.clone(), |thread, cx| {
|
||||
let out = thread
|
||||
.terminal(args.terminal_id)?
|
||||
.read(cx)
|
||||
.current_output(cx);
|
||||
|
||||
Ok(out)
|
||||
})?
|
||||
}
|
||||
|
||||
async fn wait_for_terminal_exit(
|
||||
&self,
|
||||
args: acp::WaitForTerminalExitRequest,
|
||||
) -> Result<acp::WaitForTerminalExitResponse, acp::Error> {
|
||||
let exit_status = self
|
||||
.session_thread(&args.session_id)?
|
||||
.update(&mut self.cx.clone(), |thread, cx| {
|
||||
anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit())
|
||||
})??
|
||||
.await;
|
||||
|
||||
Ok(acp::WaitForTerminalExitResponse { exit_status })
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientDelegate {
|
||||
fn session_thread(&self, session_id: &acp::SessionId) -> Result<WeakEntity<AcpThread>> {
|
||||
let sessions = self.sessions.borrow();
|
||||
sessions
|
||||
.get(session_id)
|
||||
.context("Failed to get session")
|
||||
.map(|session| session.thread.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,20 @@ pub fn init(cx: &mut App) {
|
||||
pub struct AgentServerDelegate {
|
||||
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(
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_tx: Option<watch::Sender<Option<String>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
status_tx,
|
||||
new_version_available: new_version_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
@@ -61,7 +70,7 @@ impl AgentServerDelegate {
|
||||
binary_name: SharedString,
|
||||
package_name: SharedString,
|
||||
entrypoint_path: PathBuf,
|
||||
ignore_system_version: bool,
|
||||
search_path: bool,
|
||||
minimum_version: Option<Version>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<AgentServerCommand>> {
|
||||
@@ -73,9 +82,10 @@ impl AgentServerDelegate {
|
||||
)));
|
||||
};
|
||||
let status_tx = self.status_tx;
|
||||
let new_version_available = self.new_version_available;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if !ignore_system_version {
|
||||
if search_path {
|
||||
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
|
||||
return Ok(AgentServerCommand {
|
||||
path: bin,
|
||||
@@ -101,9 +111,11 @@ impl AgentServerDelegate {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(version) = file_name
|
||||
.to_str()
|
||||
.and_then(|name| semver::Version::from_str(&name).ok())
|
||||
if let Some(name) = file_name.to_str()
|
||||
&& let Some(version) = semver::Version::from_str(name).ok()
|
||||
&& fs
|
||||
.is_file(&dir.join(file_name).join(&entrypoint_path))
|
||||
.await
|
||||
{
|
||||
versions.push((version, file_name.to_owned()));
|
||||
} else {
|
||||
@@ -146,6 +158,7 @@ impl AgentServerDelegate {
|
||||
cx.background_spawn({
|
||||
let file_name = file_name.clone();
|
||||
let dir = dir.clone();
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
let latest_version =
|
||||
node_runtime.npm_package_latest_version(&package_name).await;
|
||||
@@ -160,6 +173,9 @@ impl AgentServerDelegate {
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
if let Some(mut new_version_available) = new_version_available {
|
||||
new_version_available.send(Some(latest_version)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -171,7 +187,7 @@ impl AgentServerDelegate {
|
||||
}
|
||||
let dir = dir.clone();
|
||||
cx.background_spawn(Self::download_latest_version(
|
||||
fs,
|
||||
fs.clone(),
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
@@ -179,14 +195,18 @@ impl AgentServerDelegate {
|
||||
.await?
|
||||
.into()
|
||||
};
|
||||
|
||||
let agent_server_path = dir.join(version).join(entrypoint_path);
|
||||
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
|
||||
anyhow::ensure!(
|
||||
agent_server_path_exists,
|
||||
"Missing entrypoint path {} after installation",
|
||||
agent_server_path.to_string_lossy()
|
||||
);
|
||||
|
||||
anyhow::Ok(AgentServerCommand {
|
||||
path: node_path,
|
||||
args: vec![
|
||||
dir.join(version)
|
||||
.join(entrypoint_path)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
],
|
||||
args: vec![agent_server_path.to_string_lossy().to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, bail};
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
|
||||
@@ -26,21 +25,24 @@ impl ClaudeCode {
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<ClaudeCodeLoginCommand>> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.get("claude")
|
||||
.cloned()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
let mut command = if custom_command.is_some() {
|
||||
bail!("Cannot construct login command because a custom command was specified for claude-code-acp in settings")
|
||||
} 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,
|
||||
false,
|
||||
Some("0.2.5".parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
@@ -76,21 +78,25 @@ impl AgentServer for ClaudeCode {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.get("claude")
|
||||
.cloned()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
let mut command = if let Some(custom_command) = custom_command {
|
||||
custom_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,
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
@@ -98,16 +104,17 @@ impl AgentServer for ClaudeCode {
|
||||
.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);
|
||||
}
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
#[cfg(test)]
|
||||
use crate::{AgentServerCommand, CustomAgentServerSettings};
|
||||
use crate::AgentServerCommand;
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
@@ -473,15 +473,20 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
#[cfg(test)]
|
||||
crate::AllAgentServersSettings::override_global(
|
||||
crate::AllAgentServersSettings {
|
||||
claude: Some(CustomAgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: "claude-code-acp".into(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
},
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
custom: collections::HashMap::default(),
|
||||
commands: [
|
||||
(
|
||||
"claude".into(),
|
||||
AgentServerCommand {
|
||||
path: "claude-code-acp".into(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
},
|
||||
),
|
||||
("gemini".into(), crate::gemini::tests::local_command()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
gemini_is_system: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -498,7 +503,7 @@ pub async fn new_test_thread(
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None, None);
|
||||
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
|
||||
|
||||
@@ -2,15 +2,13 @@ use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::acp::AcpConnection;
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
|
||||
@@ -36,27 +34,27 @@ impl AgentServer for Gemini {
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
let (custom_command, is_system) = cx.read_global(|settings: &SettingsStore, _| {
|
||||
let s = settings.get::<AllAgentServersSettings>(None);
|
||||
(
|
||||
s.get("gemini").cloned(),
|
||||
AllAgentServersSettings::is_system(s, "gemini"),
|
||||
)
|
||||
});
|
||||
|
||||
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()
|
||||
let mut command = if let Some(custom_command) = custom_command
|
||||
{
|
||||
command
|
||||
custom_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,
|
||||
is_system,
|
||||
Some(Self::MINIMUM_VERSION.parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
@@ -74,6 +72,13 @@ impl AgentServer for Gemini {
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
match &result {
|
||||
Ok(connection) => {
|
||||
@@ -92,7 +97,7 @@ impl AgentServer for Gemini {
|
||||
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(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
@@ -129,7 +134,7 @@ impl AgentServer for Gemini {
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: command.path.to_string_lossy().to_string().into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
|
||||
@@ -6,20 +6,56 @@ use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<CustomAgentServerSettings>,
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent_servers")]
|
||||
pub struct AllAgentServersSettingsContent {
|
||||
gemini: Option<GeminiSettingsContent>,
|
||||
claude: Option<AgentServerCommand>,
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
|
||||
pub custom: HashMap<SharedString, AgentServerCommand>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub commands: HashMap<SharedString, AgentServerCommand>,
|
||||
pub gemini_is_system: bool,
|
||||
}
|
||||
|
||||
impl AllAgentServersSettings {
|
||||
pub fn is_system(this: &Self, name: &str) -> bool {
|
||||
if name == "gemini" {
|
||||
this.gemini_is_system
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AllAgentServersSettings {
|
||||
type Target = HashMap<SharedString, AgentServerCommand>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.commands
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for AllAgentServersSettings {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.commands
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
|
||||
pub struct GeminiSettingsContent {
|
||||
ignore_system_version: Option<bool>,
|
||||
#[serde(flatten)]
|
||||
inner: Option<AgentServerCommand>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
@@ -69,37 +105,40 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct CustomAgentServerSettings {
|
||||
pub struct AgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
type FileContent = AllAgentServersSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings {
|
||||
for AllAgentServersSettingsContent {
|
||||
gemini,
|
||||
claude,
|
||||
custom,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
if let Some(gemini) = gemini {
|
||||
if let Some(ignore) = gemini.ignore_system_version {
|
||||
settings.gemini_is_system = !ignore;
|
||||
}
|
||||
if let Some(gemini) = gemini.inner.as_ref() {
|
||||
settings.insert("gemini".into(), gemini.clone());
|
||||
}
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
if let Some(claude) = claude.clone() {
|
||||
settings.insert("claude".into(), claude);
|
||||
}
|
||||
|
||||
// Merge custom agents
|
||||
for (name, config) in custom {
|
||||
for (name, command) in custom {
|
||||
// Skip built-in agent names to avoid conflicts
|
||||
if name != "gemini" && name != "claude" {
|
||||
settings.custom.insert(name.clone(), config.clone());
|
||||
settings.commands.insert(name.clone(), command.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,3 +148,31 @@ impl settings::Settings for AllAgentServersSettings {
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{AgentServerCommand, GeminiSettingsContent};
|
||||
|
||||
#[test]
|
||||
fn test_deserialization() {
|
||||
let value = json!({
|
||||
"command": "foo",
|
||||
"args": ["bar"],
|
||||
"ignore_system_version": false
|
||||
});
|
||||
let settings = serde_json::from_value::<GeminiSettingsContent>(value).unwrap();
|
||||
assert_eq!(
|
||||
settings,
|
||||
GeminiSettingsContent {
|
||||
ignore_system_version: Some(false),
|
||||
inner: Some(AgentServerCommand {
|
||||
path: "foo".into(),
|
||||
args: vec!["bar".into()],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent", fallback_key = "assistant")]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -352,18 +353,19 @@ impl JsonSchema for LanguageModelProviderSetting {
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"enum": [
|
||||
"anthropic",
|
||||
"amazon-bedrock",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"ollama",
|
||||
"openai",
|
||||
"zed.dev",
|
||||
"anthropic",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"openrouter",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"mistral",
|
||||
"vercel"
|
||||
"ollama",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"vercel",
|
||||
"x_ai",
|
||||
"zed.dev"
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -398,10 +400,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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::cell::Cell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
@@ -13,8 +13,10 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
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;
|
||||
@@ -23,7 +25,7 @@ use ui::prelude::*;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::AgentPanel;
|
||||
use crate::acp::message_editor::MessageEditor;
|
||||
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::context_picker::file_context_picker::{FileMatch, search_files};
|
||||
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
|
||||
use crate::context_picker::symbol_context_picker::SymbolMatch;
|
||||
@@ -67,6 +69,7 @@ pub struct ContextPickerCompletionProvider {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
}
|
||||
|
||||
impl ContextPickerCompletionProvider {
|
||||
@@ -76,6 +79,7 @@ impl ContextPickerCompletionProvider {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
message_editor,
|
||||
@@ -83,6 +87,7 @@ impl ContextPickerCompletionProvider {
|
||||
history_store,
|
||||
prompt_store,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +374,42 @@ impl ContextPickerCompletionProvider {
|
||||
})
|
||||
}
|
||||
|
||||
fn search(
|
||||
fn search_slash_commands(
|
||||
&self,
|
||||
query: String,
|
||||
cx: &mut App,
|
||||
) -> Task<Vec<acp::AvailableCommand>> {
|
||||
let commands = self.available_commands.borrow().clone();
|
||||
if commands.is_empty() {
|
||||
return Task::ready(Vec::new());
|
||||
}
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, command)| StringMatchCandidate::new(id, &command.name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Arc::new(AtomicBool::default()),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| commands[mat.candidate_id].clone())
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn search_mentions(
|
||||
&self,
|
||||
mode: Option<ContextPickerMode>,
|
||||
query: String,
|
||||
@@ -651,10 +691,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let offset_to_line = buffer.point_to_offset(line_start);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
let line = lines.next()?;
|
||||
MentionCompletion::try_parse(
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
ContextCompletion::try_parse(
|
||||
line,
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
)
|
||||
});
|
||||
let Some(state) = state else {
|
||||
@@ -667,97 +707,175 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let source_range = snapshot.anchor_before(state.source_range.start)
|
||||
..snapshot.anchor_after(state.source_range.end);
|
||||
let source_range = snapshot.anchor_before(state.source_range().start)
|
||||
..snapshot.anchor_after(state.source_range().end);
|
||||
|
||||
let editor = self.message_editor.clone();
|
||||
|
||||
let MentionCompletion { mode, argument, .. } = state;
|
||||
let query = argument.unwrap_or_else(|| "".to_string());
|
||||
|
||||
let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let matches = search_task.await;
|
||||
|
||||
let completions = cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| match mat {
|
||||
Match::File(FileMatch { mat, is_recent }) => {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
match state {
|
||||
ContextCompletion::SlashCommand(SlashCommandCompletion {
|
||||
command, argument, ..
|
||||
}) => {
|
||||
let search_task = self.search_slash_commands(command.unwrap_or_default(), cx);
|
||||
cx.background_spawn(async move {
|
||||
let completions = search_task
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|command| {
|
||||
let new_text = if let Some(argument) = argument.as_ref() {
|
||||
format!("/{} {}", command.name, argument)
|
||||
} else {
|
||||
format!("/{} ", command.name)
|
||||
};
|
||||
|
||||
Self::completion_for_path(
|
||||
project_path,
|
||||
&mat.path_prefix,
|
||||
is_recent,
|
||||
mat.is_dir,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
let is_missing_argument = argument.is_none() && command.input.is_some();
|
||||
Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label: CodeLabel::plain(command.name.to_string(), None),
|
||||
documentation: Some(CompletionDocumentation::MultiLinePlainText(
|
||||
command.description.into(),
|
||||
)),
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: None,
|
||||
insert_text_mode: None,
|
||||
confirm: Some(Arc::new({
|
||||
let editor = editor.clone();
|
||||
move |intent, _window, cx| {
|
||||
if !is_missing_argument {
|
||||
cx.defer({
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |_editor, cx| {
|
||||
match intent {
|
||||
CompletionIntent::Complete
|
||||
| CompletionIntent::CompleteWithInsert
|
||||
| CompletionIntent::CompleteWithReplace => {
|
||||
if !is_missing_argument {
|
||||
cx.emit(MessageEditorEvent::Send);
|
||||
}
|
||||
}
|
||||
CompletionIntent::Compose => {}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
is_missing_argument
|
||||
}
|
||||
})),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
|
||||
symbol,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
),
|
||||
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,
|
||||
}])
|
||||
})
|
||||
}
|
||||
ContextCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
|
||||
let query = argument.unwrap_or_default();
|
||||
let search_task =
|
||||
self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
|
||||
|
||||
Match::Thread(thread) => Some(Self::completion_for_thread(
|
||||
thread,
|
||||
source_range.clone(),
|
||||
false,
|
||||
editor.clone(),
|
||||
cx,
|
||||
)),
|
||||
cx.spawn(async move |_, cx| {
|
||||
let matches = search_task.await;
|
||||
|
||||
Match::RecentThread(thread) => Some(Self::completion_for_thread(
|
||||
thread,
|
||||
source_range.clone(),
|
||||
true,
|
||||
editor.clone(),
|
||||
cx,
|
||||
)),
|
||||
let completions = cx.update(|cx| {
|
||||
matches
|
||||
.into_iter()
|
||||
.filter_map(|mat| match mat {
|
||||
Match::File(FileMatch { mat, is_recent }) => {
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: WorktreeId::from_usize(mat.worktree_id),
|
||||
path: mat.path.clone(),
|
||||
};
|
||||
|
||||
Match::Rules(user_rules) => Some(Self::completion_for_rules(
|
||||
user_rules,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
cx,
|
||||
)),
|
||||
Self::completion_for_path(
|
||||
project_path,
|
||||
&mat.path_prefix,
|
||||
is_recent,
|
||||
mat.is_dir,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
project.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
Match::Fetch(url) => Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
editor.clone(),
|
||||
cx,
|
||||
),
|
||||
Match::Symbol(SymbolMatch { symbol, .. }) => {
|
||||
Self::completion_for_symbol(
|
||||
symbol,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
|
||||
entry,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
&workspace,
|
||||
cx,
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
Match::Thread(thread) => Some(Self::completion_for_thread(
|
||||
thread,
|
||||
source_range.clone(),
|
||||
false,
|
||||
editor.clone(),
|
||||
cx,
|
||||
)),
|
||||
|
||||
Ok(vec![CompletionResponse {
|
||||
completions,
|
||||
// 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,
|
||||
}])
|
||||
})
|
||||
Match::RecentThread(thread) => Some(Self::completion_for_thread(
|
||||
thread,
|
||||
source_range.clone(),
|
||||
true,
|
||||
editor.clone(),
|
||||
cx,
|
||||
)),
|
||||
|
||||
Match::Rules(user_rules) => Some(Self::completion_for_rules(
|
||||
user_rules,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
cx,
|
||||
)),
|
||||
|
||||
Match::Fetch(url) => Self::completion_for_fetch(
|
||||
source_range.clone(),
|
||||
url,
|
||||
editor.clone(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Match::Entry(EntryMatch { entry, .. }) => {
|
||||
Self::completion_for_entry(
|
||||
entry,
|
||||
source_range.clone(),
|
||||
editor.clone(),
|
||||
&workspace,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})?;
|
||||
|
||||
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,
|
||||
}])
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
@@ -775,14 +893,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let offset_to_line = buffer.point_to_offset(line_start);
|
||||
let mut lines = buffer.text_for_range(line_start..position).lines();
|
||||
if let Some(line) = lines.next() {
|
||||
MentionCompletion::try_parse(
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
ContextCompletion::try_parse(
|
||||
line,
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.get().embedded_context,
|
||||
)
|
||||
.map(|completion| {
|
||||
completion.source_range.start <= offset_to_line + position.column as usize
|
||||
&& completion.source_range.end >= offset_to_line + position.column as usize
|
||||
completion.source_range().start <= offset_to_line + position.column as usize
|
||||
&& completion.source_range().end >= offset_to_line + position.column as usize
|
||||
})
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
@@ -851,7 +969,7 @@ fn confirm_completion_callback(
|
||||
.clone()
|
||||
.update(cx, |message_editor, cx| {
|
||||
message_editor
|
||||
.confirm_completion(
|
||||
.confirm_mention_completion(
|
||||
crease_text,
|
||||
start,
|
||||
content_len,
|
||||
@@ -867,6 +985,89 @@ fn confirm_completion_callback(
|
||||
})
|
||||
}
|
||||
|
||||
enum ContextCompletion {
|
||||
SlashCommand(SlashCommandCompletion),
|
||||
Mention(MentionCompletion),
|
||||
}
|
||||
|
||||
impl ContextCompletion {
|
||||
fn source_range(&self) -> Range<usize> {
|
||||
match self {
|
||||
Self::SlashCommand(completion) => completion.source_range.clone(),
|
||||
Self::Mention(completion) => completion.source_range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option<Self> {
|
||||
if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) {
|
||||
Some(Self::SlashCommand(command))
|
||||
} else if let Some(mention) =
|
||||
MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line)
|
||||
{
|
||||
Some(Self::Mention(mention))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct SlashCommandCompletion {
|
||||
pub source_range: Range<usize>,
|
||||
pub command: Option<String>,
|
||||
pub argument: Option<String>,
|
||||
}
|
||||
|
||||
impl SlashCommandCompletion {
|
||||
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())
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
source_range: last_command_start + offset_to_line..end + offset_to_line,
|
||||
command,
|
||||
argument,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
struct MentionCompletion {
|
||||
source_range: Range<usize>,
|
||||
@@ -932,6 +1133,62 @@ impl MentionCompletion {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_slash_command_completion_parse() {
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..1,
|
||||
command: None,
|
||||
argument: None,
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/help", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..5,
|
||||
command: Some("help".to_string()),
|
||||
argument: None,
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/help ", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..5,
|
||||
command: Some("help".to_string()),
|
||||
argument: None,
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/help arg1", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..10,
|
||||
command: Some("help".to_string()),
|
||||
argument: Some("arg1".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/help arg1 arg2", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..15,
|
||||
command: Some("help".to_string()),
|
||||
argument: Some("arg1 arg2".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mention_completion_parse() {
|
||||
assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use std::{cell::Cell, ops::Range, rc::Rc};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use acp_thread::{AcpThread, AgentThreadEntry};
|
||||
use agent_client_protocol::{PromptCapabilities, ToolCallId};
|
||||
use agent_client_protocol::{self as acp, ToolCallId};
|
||||
use agent2::HistoryStore;
|
||||
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;
|
||||
@@ -26,8 +30,9 @@ pub struct EntryViewState {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
entries: Vec<Entry>,
|
||||
prevent_slash_commands: bool,
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
}
|
||||
|
||||
impl EntryViewState {
|
||||
@@ -36,8 +41,9 @@ impl EntryViewState {
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
prevent_slash_commands: bool,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
@@ -45,8 +51,9 @@ impl EntryViewState {
|
||||
history_store,
|
||||
prompt_store,
|
||||
entries: Vec::new(),
|
||||
prevent_slash_commands,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
agent_name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +92,9 @@ impl EntryViewState {
|
||||
self.history_store.clone(),
|
||||
self.prompt_store.clone(),
|
||||
self.prompt_capabilities.clone(),
|
||||
self.available_commands.clone(),
|
||||
self.agent_name.clone(),
|
||||
"Edit message - @ to include context",
|
||||
self.prevent_slash_commands,
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
@@ -125,22 +133,35 @@ impl EntryViewState {
|
||||
views
|
||||
};
|
||||
|
||||
let is_tool_call_completed =
|
||||
matches!(tool_call.status, acp_thread::ToolCallStatus::Completed);
|
||||
|
||||
for terminal in terminals {
|
||||
views.entry(terminal.entity_id()).or_insert_with(|| {
|
||||
let element = create_terminal(
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
terminal.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any();
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::NewTerminal(id.clone()),
|
||||
});
|
||||
element
|
||||
});
|
||||
match views.entry(terminal.entity_id()) {
|
||||
collections::hash_map::Entry::Vacant(entry) => {
|
||||
let element = create_terminal(
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
terminal.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any();
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::NewTerminal(id.clone()),
|
||||
});
|
||||
entry.insert(element);
|
||||
}
|
||||
collections::hash_map::Entry::Occupied(_entry) => {
|
||||
if is_tool_call_completed && terminal.read(cx).output().is_none() {
|
||||
cx.emit(EntryViewEvent {
|
||||
entry_index: index,
|
||||
view_event: ViewEvent::TerminalMovedToBackground(id.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for diff in diffs {
|
||||
@@ -186,7 +207,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 { .. } => {}
|
||||
@@ -217,6 +238,7 @@ pub struct EntryViewEvent {
|
||||
pub enum ViewEvent {
|
||||
NewDiff(ToolCallId),
|
||||
NewTerminal(ToolCallId),
|
||||
TerminalMovedToBackground(ToolCallId),
|
||||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||
}
|
||||
|
||||
@@ -457,7 +479,8 @@ mod tests {
|
||||
history_store,
|
||||
None,
|
||||
Default::default(),
|
||||
false,
|
||||
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};
|
||||
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
|
||||
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
|
||||
SemanticsProvider, 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 +22,20 @@ use futures::{
|
||||
};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, 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,
|
||||
cell::{Cell, RefCell},
|
||||
ffi::OsStr,
|
||||
fmt::Write,
|
||||
ops::{Range, RangeInclusive},
|
||||
@@ -42,20 +44,18 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use text::{OffsetRangeExt, ToOffset as _};
|
||||
use text::OffsetRangeExt;
|
||||
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, px,
|
||||
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, Styled, TextSize, TintColor,
|
||||
Toggleable, Window, div, h_flex,
|
||||
};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{Workspace, notifications::NotifyResultExt as _};
|
||||
use zed_actions::agent::Chat;
|
||||
|
||||
const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
pub struct MessageEditor {
|
||||
mention_set: MentionSet,
|
||||
editor: Entity<Editor>,
|
||||
@@ -63,8 +63,9 @@ pub struct MessageEditor {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prevent_slash_commands: bool,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
_parse_slash_command_task: Task<()>,
|
||||
}
|
||||
@@ -79,6 +80,8 @@ pub enum MessageEditorEvent {
|
||||
|
||||
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
|
||||
|
||||
const COMMAND_HINT_INLAY_ID: usize = 0;
|
||||
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -86,8 +89,9 @@ impl MessageEditor {
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
agent_name: SharedString,
|
||||
placeholder: impl Into<Arc<str>>,
|
||||
prevent_slash_commands: bool,
|
||||
mode: EditorMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -99,16 +103,14 @@ impl MessageEditor {
|
||||
},
|
||||
None,
|
||||
);
|
||||
let completion_provider = ContextPickerCompletionProvider::new(
|
||||
let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
|
||||
cx.weak_entity(),
|
||||
workspace.clone(),
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
);
|
||||
let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
|
||||
range: Cell::new(None),
|
||||
});
|
||||
available_commands.clone(),
|
||||
));
|
||||
let mention_set = MentionSet::default();
|
||||
let editor = cx.new(|cx| {
|
||||
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
|
||||
@@ -119,15 +121,12 @@ impl MessageEditor {
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_use_modal_editing(true);
|
||||
editor.set_completion_provider(Some(Rc::new(completion_provider)));
|
||||
editor.set_completion_provider(Some(completion_provider.clone()));
|
||||
editor.set_context_menu_options(ContextMenuOptions {
|
||||
min_entries_visible: 12,
|
||||
max_entries_visible: 12,
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
if prevent_slash_commands {
|
||||
editor.set_semantics_provider(Some(semantics_provider.clone()));
|
||||
}
|
||||
editor.register_addon(MessageEditorAddon::new());
|
||||
editor
|
||||
});
|
||||
@@ -141,21 +140,33 @@ impl MessageEditor {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut has_hint = false;
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
let semantics_provider = semantics_provider.clone();
|
||||
move |this, editor, event, window, cx| {
|
||||
if let EditorEvent::Edited { .. } = event {
|
||||
if prevent_slash_commands {
|
||||
this.highlight_slash_command(
|
||||
semantics_provider.clone(),
|
||||
editor.clone(),
|
||||
window,
|
||||
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,
|
||||
);
|
||||
}
|
||||
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
has_hint = has_new_hint;
|
||||
|
||||
editor.snapshot(window, cx)
|
||||
});
|
||||
this.mention_set.remove_invalid(snapshot);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -168,13 +179,57 @@ impl MessageEditor {
|
||||
workspace,
|
||||
history_store,
|
||||
prompt_store,
|
||||
prevent_slash_commands,
|
||||
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,
|
||||
@@ -191,7 +246,7 @@ impl MessageEditor {
|
||||
.text_anchor
|
||||
});
|
||||
|
||||
self.confirm_completion(
|
||||
self.confirm_mention_completion(
|
||||
thread.title.clone(),
|
||||
start,
|
||||
thread.title.len(),
|
||||
@@ -227,7 +282,7 @@ impl MessageEditor {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn confirm_completion(
|
||||
pub fn confirm_mention_completion(
|
||||
&mut self,
|
||||
crease_text: SharedString,
|
||||
start: text::Anchor,
|
||||
@@ -645,7 +700,7 @@ impl MessageEditor {
|
||||
self.project.read(cx).fs().clone(),
|
||||
self.history_store.clone(),
|
||||
));
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
|
||||
let connection = server.connect(Path::new(""), delegate, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let agent = connection.await?;
|
||||
@@ -679,21 +734,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);
|
||||
let editor = self.editor.clone();
|
||||
let prevent_slash_commands = self.prevent_slash_commands;
|
||||
|
||||
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);
|
||||
@@ -706,14 +802,16 @@ impl MessageEditor {
|
||||
|
||||
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
|
||||
if crease_range.start > ix {
|
||||
let chunk = if prevent_slash_commands
|
||||
&& ix == 0
|
||||
&& parse_slash_command(&text[ix..]).is_some()
|
||||
{
|
||||
format!(" {}", &text[ix..crease_range.start]).into()
|
||||
} else {
|
||||
text[ix..crease_range.start].into()
|
||||
};
|
||||
//todo(): Custom slash command ContentBlock?
|
||||
// let chunk = if prevent_slash_commands
|
||||
// && ix == 0
|
||||
// && parse_slash_command(&text[ix..]).is_some()
|
||||
// {
|
||||
// format!(" {}", &text[ix..crease_range.start]).into()
|
||||
// } else {
|
||||
// text[ix..crease_range.start].into()
|
||||
// };
|
||||
let chunk = text[ix..crease_range.start].into();
|
||||
chunks.push(chunk);
|
||||
}
|
||||
let chunk = match mention {
|
||||
@@ -769,22 +867,24 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
if ix < text.len() {
|
||||
let last_chunk = if prevent_slash_commands
|
||||
&& ix == 0
|
||||
&& parse_slash_command(&text[ix..]).is_some()
|
||||
{
|
||||
format!(" {}", text[ix..].trim_end())
|
||||
} else {
|
||||
text[ix..].trim_end().to_owned()
|
||||
};
|
||||
//todo(): Custom slash command ContentBlock?
|
||||
// let last_chunk = if prevent_slash_commands
|
||||
// && ix == 0
|
||||
// && parse_slash_command(&text[ix..]).is_some()
|
||||
// {
|
||||
// format!(" {}", text[ix..].trim_end())
|
||||
// } else {
|
||||
// text[ix..].trim_end().to_owned()
|
||||
// };
|
||||
let last_chunk = text[ix..].trim_end().to_owned();
|
||||
if !last_chunk.is_empty() {
|
||||
chunks.push(last_chunk.into());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(chunks, all_tracked_buffers)
|
||||
})
|
||||
Ok((chunks, all_tracked_buffers))
|
||||
})?;
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -971,7 +1071,14 @@ impl MessageEditor {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx));
|
||||
tasks.push(self.confirm_mention_completion(
|
||||
file_name,
|
||||
anchor,
|
||||
content_len,
|
||||
uri,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
cx.spawn(async move |_, _| {
|
||||
join_all(tasks).await;
|
||||
@@ -1133,48 +1240,6 @@ impl MessageEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn highlight_slash_command(
|
||||
&mut self,
|
||||
semantics_provider: Rc<SlashCommandSemanticsProvider>,
|
||||
editor: Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
struct InvalidSlashCommand;
|
||||
|
||||
self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(PARSE_SLASH_COMMAND_DEBOUNCE)
|
||||
.await;
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let range = parse_slash_command(&editor.text(cx));
|
||||
semantics_provider.range.set(range);
|
||||
if let Some((start, end)) = range {
|
||||
editor.highlight_text::<InvalidSlashCommand>(
|
||||
vec![
|
||||
snapshot.buffer_snapshot.anchor_after(start)
|
||||
..snapshot.buffer_snapshot.anchor_before(end),
|
||||
],
|
||||
HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(gpui::red()),
|
||||
wavy: true,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
editor.clear_highlights::<InvalidSlashCommand>(cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
pub fn text(&self, cx: &App) -> String {
|
||||
self.editor.read(cx).text(cx)
|
||||
}
|
||||
@@ -1234,6 +1299,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()
|
||||
},
|
||||
)
|
||||
@@ -1264,7 +1330,7 @@ pub(crate) fn insert_crease_for_mention(
|
||||
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
|
||||
|
||||
let placeholder = FoldPlaceholder {
|
||||
render: render_fold_icon_button(
|
||||
render: render_mention_fold_button(
|
||||
crease_label,
|
||||
crease_icon,
|
||||
start..end,
|
||||
@@ -1294,7 +1360,7 @@ pub(crate) fn insert_crease_for_mention(
|
||||
Some((crease_id, tx))
|
||||
}
|
||||
|
||||
fn render_fold_icon_button(
|
||||
fn render_mention_fold_button(
|
||||
label: SharedString,
|
||||
icon: SharedString,
|
||||
range: Range<Anchor>,
|
||||
@@ -1471,118 +1537,6 @@ impl MentionSet {
|
||||
}
|
||||
}
|
||||
|
||||
struct SlashCommandSemanticsProvider {
|
||||
range: Cell<Option<(usize, usize)>>,
|
||||
}
|
||||
|
||||
impl SemanticsProvider for SlashCommandSemanticsProvider {
|
||||
fn hover(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: text::Anchor,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Vec<project::Hover>>> {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let offset = position.to_offset(&snapshot);
|
||||
let (start, end) = self.range.get()?;
|
||||
if !(start..end).contains(&offset) {
|
||||
return None;
|
||||
}
|
||||
let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
|
||||
Some(Task::ready(vec![project::Hover {
|
||||
contents: vec![project::HoverBlock {
|
||||
text: "Slash commands are not supported".into(),
|
||||
kind: project::HoverBlockKind::PlainText,
|
||||
}],
|
||||
range: Some(range),
|
||||
language: None,
|
||||
}]))
|
||||
}
|
||||
|
||||
fn inline_values(
|
||||
&self,
|
||||
_buffer_handle: Entity<Buffer>,
|
||||
_range: Range<text::Anchor>,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn inlay_hints(
|
||||
&self,
|
||||
_buffer_handle: Entity<Buffer>,
|
||||
_range: Range<text::Anchor>,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn resolve_inlay_hint(
|
||||
&self,
|
||||
_hint: project::InlayHint,
|
||||
_buffer_handle: Entity<Buffer>,
|
||||
_server_id: lsp::LanguageServerId,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn document_highlights(
|
||||
&self,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_position: text::Anchor,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn definitions(
|
||||
&self,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_position: text::Anchor,
|
||||
_kind: editor::GotoDefinitionKind,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<Result<Vec<project::LocationLink>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn range_for_rename(
|
||||
&self,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_position: text::Anchor,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn perform_rename(
|
||||
&self,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_position: text::Anchor,
|
||||
_new_name: String,
|
||||
_cx: &mut App,
|
||||
) -> Option<Task<Result<project::ProjectTransaction>>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_slash_command(text: &str) -> Option<(usize, usize)> {
|
||||
if let Some(remainder) = text.strip_prefix('/') {
|
||||
let pos = remainder
|
||||
.find(char::is_whitespace)
|
||||
.unwrap_or(remainder.len());
|
||||
let command = &remainder[..pos];
|
||||
if !command.is_empty() && command.chars().all(char::is_alphanumeric) {
|
||||
return Some((0, 1 + command.len()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub struct MessageEditorAddon {}
|
||||
|
||||
impl MessageEditorAddon {
|
||||
@@ -1610,7 +1564,13 @@ impl Addon for MessageEditorAddon {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
ops::Range,
|
||||
path::Path,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use acp_thread::MentionUri;
|
||||
use agent_client_protocol as acp;
|
||||
@@ -1657,8 +1617,9 @@ mod tests {
|
||||
history_store.clone(),
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
false,
|
||||
EditorMode::AutoHeight {
|
||||
min_lines: 1,
|
||||
max_lines: None,
|
||||
@@ -1735,6 +1696,140 @@ 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(Cell::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,
|
||||
}]);
|
||||
|
||||
// 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 {
|
||||
@@ -1764,7 +1859,192 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_completion_provider(cx: &mut TestAppContext) {
|
||||
async fn test_completion_provider_commands(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let workspace = window.root(cx).unwrap();
|
||||
|
||||
let mut cx = VisualTestContext::from_window(*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 prompt_capabilities = Rc::new(Cell::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,
|
||||
},
|
||||
acp::AvailableCommand {
|
||||
name: "say-hello".to_string(),
|
||||
description: "Say hello to whoever you want".to_string(),
|
||||
input: Some(acp::AvailableCommandInput::Unstructured {
|
||||
hint: "<name>".to_string(),
|
||||
}),
|
||||
},
|
||||
]));
|
||||
|
||||
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.weak_entity();
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
workspace_handle,
|
||||
project.clone(),
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
min_lines: 1,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(
|
||||
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
message_editor.read(cx).focus_handle(cx).focus(window);
|
||||
message_editor.read(cx).editor().clone()
|
||||
});
|
||||
|
||||
cx.simulate_input("/");
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
current_completion_labels_with_documentation(editor),
|
||||
&[
|
||||
("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
|
||||
("say-hello".into(), "Say hello to whoever you want".into())
|
||||
]
|
||||
);
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
|
||||
cx.simulate_input("/qui");
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/qui");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
current_completion_labels_with_documentation(editor),
|
||||
&[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
|
||||
);
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.display_text(cx), "/quick-math ");
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
|
||||
cx.simulate_input("/say");
|
||||
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.display_text(cx), "/say");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
current_completion_labels_with_documentation(editor),
|
||||
&[("say-hello".into(), "Say hello to whoever you want".into())]
|
||||
);
|
||||
});
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
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!(
|
||||
current_completion_labels_with_documentation(editor),
|
||||
&[("say-hello".into(), "Say hello to whoever you want".into())]
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_input("GPT5");
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
@@ -1857,8 +2137,9 @@ mod tests {
|
||||
history_store.clone(),
|
||||
None,
|
||||
prompt_capabilities.clone(),
|
||||
Default::default(),
|
||||
"Test Agent".into(),
|
||||
"Test",
|
||||
false,
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: None,
|
||||
min_lines: 1,
|
||||
@@ -1888,7 +2169,6 @@ mod tests {
|
||||
assert_eq!(editor.text(cx), "Lorem @");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
// Only files since we have default capabilities
|
||||
assert_eq!(
|
||||
current_completion_labels(editor),
|
||||
&[
|
||||
@@ -2128,7 +2408,7 @@ mod tests {
|
||||
lsp::SymbolInformation {
|
||||
name: "MySymbol".into(),
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
|
||||
range: lsp::Range::new(
|
||||
lsp::Position::new(0, 0),
|
||||
lsp::Position::new(0, 1),
|
||||
@@ -2284,4 +2564,20 @@ mod tests {
|
||||
.map(|completion| completion.label.text)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
|
||||
let completions = editor.current_completions().expect("Missing completions");
|
||||
completions
|
||||
.into_iter()
|
||||
.map(|completion| {
|
||||
(
|
||||
completion.label.text,
|
||||
completion
|
||||
.documentation
|
||||
.map(|d| d.text().to_string())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ impl AcpModelPickerDelegate {
|
||||
let (models, selected_model) = futures::join!(models_task, selected_model_task);
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.models = models.log_err();
|
||||
this.delegate.models = models.ok();
|
||||
this.delegate.selected_model = selected_model.ok();
|
||||
this.refresh(window, cx)
|
||||
})
|
||||
@@ -141,11 +141,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = match this
|
||||
.read_with(cx, |this, cx| {
|
||||
if let Some(models) = this.delegate.models.as_ref() {
|
||||
log::debug!("Filtering {} models.", models.len());
|
||||
} else {
|
||||
log::debug!("No models available.");
|
||||
}
|
||||
this.delegate.models.clone().map(move |models| {
|
||||
fuzzy_search(models, query, cx.background_executor().clone())
|
||||
})
|
||||
@@ -157,8 +152,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
None => AgentModelList::Flat(vec![]),
|
||||
};
|
||||
|
||||
log::debug!("Filtered models. {} available.", filtered_models.len());
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
info_list_to_picker_entries(filtered_models).collect();
|
||||
|
||||
@@ -36,6 +36,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 {
|
||||
|
||||
@@ -8,7 +8,7 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, PromptCapabilities};
|
||||
use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use audio::{Audio, Sound};
|
||||
use buffer_diff::BufferDiff;
|
||||
@@ -23,9 +23,9 @@ use gpui::{
|
||||
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
||||
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
|
||||
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
|
||||
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
|
||||
point, prelude::*, pulsating_between,
|
||||
Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
|
||||
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
|
||||
pulsating_between,
|
||||
};
|
||||
use language::Buffer;
|
||||
|
||||
@@ -35,7 +35,7 @@ use project::{Project, ProjectEntryId};
|
||||
use prompt_store::{PromptId, PromptStore};
|
||||
use rope::Point;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::cell::Cell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -43,10 +43,10 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||
use task::SpawnInTerminal;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use theme::{AgentFontSize, ThemeSettings};
|
||||
use ui::{
|
||||
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
||||
Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
|
||||
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
|
||||
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
@@ -78,10 +78,12 @@ enum ThreadFeedback {
|
||||
Negative,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ThreadError {
|
||||
PaymentRequired,
|
||||
ModelRequestLimitReached(cloud_llm_client::Plan),
|
||||
ToolUseLimitReached,
|
||||
Refusal,
|
||||
AuthenticationRequired(SharedString),
|
||||
Other(SharedString),
|
||||
}
|
||||
@@ -284,9 +286,11 @@ pub struct AcpThreadView {
|
||||
should_be_following: bool,
|
||||
editing_message: Option<usize>,
|
||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
|
||||
is_loading_contents: bool,
|
||||
new_server_version_available: Option<SharedString>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
_subscriptions: [Subscription; 4],
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -325,10 +329,15 @@ impl AcpThreadView {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
|
||||
let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
|
||||
let available_commands = Rc::new(RefCell::new(vec![]));
|
||||
|
||||
let placeholder = if agent.name() == "Zed Agent" {
|
||||
format!("Message the {} — @ to include context", agent.name())
|
||||
} else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
|
||||
format!(
|
||||
"Message {} — @ to include context, / for commands",
|
||||
agent.name()
|
||||
)
|
||||
} else {
|
||||
format!("Message {} — @ to include context", agent.name())
|
||||
};
|
||||
@@ -340,8 +349,9 @@ impl AcpThreadView {
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
available_commands.clone(),
|
||||
agent.name(),
|
||||
placeholder,
|
||||
prevent_slash_commands,
|
||||
editor::EditorMode::AutoHeight {
|
||||
min_lines: MIN_EDITOR_LINES,
|
||||
max_lines: Some(MAX_EDITOR_LINES),
|
||||
@@ -364,12 +374,14 @@ impl AcpThreadView {
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
prevent_slash_commands,
|
||||
available_commands.clone(),
|
||||
agent.name(),
|
||||
)
|
||||
});
|
||||
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
|
||||
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
|
||||
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
|
||||
];
|
||||
@@ -396,18 +408,34 @@ impl AcpThreadView {
|
||||
editing_message: None,
|
||||
edits_expanded: false,
|
||||
plan_expanded: false,
|
||||
prompt_capabilities,
|
||||
available_commands,
|
||||
editor_expanded: false,
|
||||
should_be_following: false,
|
||||
history_store,
|
||||
hovered_recent_history_item: None,
|
||||
prompt_capabilities,
|
||||
is_loading_contents: false,
|
||||
_subscriptions: subscriptions,
|
||||
_cancel_task: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_state = Self::initial_state(
|
||||
self.agent.clone(),
|
||||
None,
|
||||
self.workspace.clone(),
|
||||
self.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
self.available_commands.replace(vec![]);
|
||||
self.new_server_version_available.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn initial_state(
|
||||
agent: Rc<dyn AgentServer>,
|
||||
resume_thread: Option<DbThreadMetadata>,
|
||||
@@ -416,14 +444,37 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ThreadState {
|
||||
let root_dir = project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
if !project.read(cx).is_local() && agent.clone().downcast::<NativeAgentServer>().is_none() {
|
||||
return ThreadState::LoadError(LoadError::Other(
|
||||
"External agents are not yet supported for remote projects.".into(),
|
||||
));
|
||||
}
|
||||
let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
// Pick the first non-single-file worktree for the root directory if there are any,
|
||||
// and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
|
||||
worktrees.sort_by(|l, r| {
|
||||
l.read(cx)
|
||||
.is_single_file()
|
||||
.cmp(&r.read(cx).is_single_file())
|
||||
});
|
||||
let root_dir = worktrees
|
||||
.into_iter()
|
||||
.filter_map(|worktree| {
|
||||
if worktree.read(cx).is_single_file() {
|
||||
Some(worktree.read(cx).abs_path().parent()?.into())
|
||||
} else {
|
||||
Some(worktree.read(cx).abs_path())
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
let (tx, mut rx) = watch::channel("Loading…".into());
|
||||
let delegate = AgentServerDelegate::new(project.clone(), Some(tx));
|
||||
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
|
||||
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
|
||||
let delegate = AgentServerDelegate::new(
|
||||
project.clone(),
|
||||
Some(status_tx),
|
||||
Some(new_version_available_tx),
|
||||
);
|
||||
|
||||
let connect_task = agent.connect(&root_dir, delegate, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
@@ -578,10 +629,23 @@ impl AcpThreadView {
|
||||
.log_err();
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Ok(new_version) = new_version_available_rx.recv().await {
|
||||
if let Some(new_version) = new_version {
|
||||
this.update(cx, |this, cx| {
|
||||
this.new_server_version_available = Some(new_version.into());
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let loading_view = cx.new(|cx| {
|
||||
let update_title_task = cx.spawn(async move |this, cx| {
|
||||
loop {
|
||||
let status = rx.recv().await?;
|
||||
let status = status_rx.recv().await?;
|
||||
this.update(cx, |this: &mut LoadingView, cx| {
|
||||
this.title = status;
|
||||
cx.notify();
|
||||
@@ -617,17 +681,13 @@ impl AcpThreadView {
|
||||
move |_, ev, window, cx| {
|
||||
if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
|
||||
&& &provider_id == updated_provider_id
|
||||
&& LanguageModelRegistry::global(cx)
|
||||
.read(cx)
|
||||
.provider(&provider_id)
|
||||
.map_or(false, |provider| provider.is_authenticated(cx))
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent.clone(),
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
this.reset(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -827,6 +887,9 @@ impl AcpThreadView {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
|
||||
self.expanded_tool_calls.remove(tool_call_id);
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
|
||||
if let Some(thread) = self.thread()
|
||||
&& let Some(AgentThreadEntry::UserMessage(user_message)) =
|
||||
@@ -899,6 +962,40 @@ impl AcpThreadView {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.message_editor.read(cx).text(cx);
|
||||
let text = text.trim();
|
||||
if text == "/login" || text == "/logout" {
|
||||
let ThreadState::Ready { thread, .. } = &self.thread_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let connection = thread.read(cx).connection().clone();
|
||||
if !connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
return;
|
||||
};
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
@@ -1189,6 +1286,14 @@ impl AcpThreadView {
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AcpThreadEvent::Refusal => {
|
||||
self.thread_retry_status.take();
|
||||
self.thread_error = Some(ThreadError::Refusal);
|
||||
let model_or_agent_name = self.get_current_model_name(cx);
|
||||
let notification_message =
|
||||
format!("{} refused to respond to this request", model_or_agent_name);
|
||||
self.notify_with_sound(¬ification_message, IconName::Warning, window, cx);
|
||||
}
|
||||
AcpThreadEvent::Error => {
|
||||
self.thread_retry_status.take();
|
||||
self.notify_with_sound(
|
||||
@@ -1220,6 +1325,30 @@ impl AcpThreadView {
|
||||
.set(thread.read(cx).prompt_capabilities());
|
||||
}
|
||||
AcpThreadEvent::TokenUsageUpdated => {}
|
||||
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
|
||||
let mut available_commands = available_commands.clone();
|
||||
|
||||
if thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "login".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
available_commands.push(acp::AvailableCommand {
|
||||
name: "logout".to_owned(),
|
||||
description: "Authenticate".to_owned(),
|
||||
input: None,
|
||||
});
|
||||
}
|
||||
|
||||
self.available_commands.replace(available_commands);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1271,11 +1400,11 @@ impl AcpThreadView {
|
||||
.read(cx)
|
||||
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
|
||||
.unwrap();
|
||||
if !provider.is_authenticated(cx) {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
let connection = connection.clone();
|
||||
window.defer(cx, move |window, cx| {
|
||||
if !provider.is_authenticated(cx) {
|
||||
Self::handle_auth_required(
|
||||
this,
|
||||
AuthRequired {
|
||||
@@ -1287,9 +1416,21 @@ impl AcpThreadView {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
this.project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if method.0.as_ref() == "vertex-ai"
|
||||
&& std::env::var("GOOGLE_API_KEY").is_err()
|
||||
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
|
||||
@@ -1333,7 +1474,6 @@ impl AcpThreadView {
|
||||
cx.notify();
|
||||
self.auth_task =
|
||||
Some(cx.spawn_in(window, {
|
||||
let project = self.project.clone();
|
||||
let agent = self.agent.clone();
|
||||
async move |this, cx| {
|
||||
let result = authenticate.await;
|
||||
@@ -1362,14 +1502,7 @@ impl AcpThreadView {
|
||||
}
|
||||
this.handle_thread_error(err, cx);
|
||||
} else {
|
||||
this.thread_state = Self::initial_state(
|
||||
agent,
|
||||
None,
|
||||
this.workspace.clone(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
this.reset(window, cx);
|
||||
}
|
||||
this.auth_task.take()
|
||||
})
|
||||
@@ -1391,7 +1524,7 @@ impl AcpThreadView {
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None);
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
|
||||
let command = ClaudeCode::login_command(delegate, cx);
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
@@ -2418,7 +2551,8 @@ impl AcpThreadView {
|
||||
|
||||
let output = terminal_data.output();
|
||||
let command_finished = output.is_some();
|
||||
let truncated_output = output.is_some_and(|output| output.was_content_truncated);
|
||||
let truncated_output =
|
||||
output.is_some_and(|output| output.original_content_len > output.content.len());
|
||||
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
|
||||
|
||||
let command_failed = command_finished
|
||||
@@ -2506,48 +2640,20 @@ impl AcpThreadView {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
),
|
||||
.with_rotate_animation(2)
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Disclosure::new(
|
||||
SharedString::from(format!(
|
||||
"terminal-tool-disclosure-{}",
|
||||
terminal.entity_id()
|
||||
)),
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&header_group)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.when(truncated_output, |header| {
|
||||
let tooltip = if let Some(output) = output {
|
||||
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||
"Output exceeded terminal max lines and was \
|
||||
truncated, the model received the first 16 KB."
|
||||
.to_string()
|
||||
format!("Output exceeded terminal max lines and was \
|
||||
truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
|
||||
} else {
|
||||
format!(
|
||||
"Output is {} long, and to avoid unexpected token usage, \
|
||||
only 16 KB was sent back to the model.",
|
||||
only {} was sent back to the agent.",
|
||||
format_file_size(output.original_content_len as u64, true),
|
||||
format_file_size(output.content.len() as u64, true)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -2595,7 +2701,29 @@ impl AcpThreadView {
|
||||
)))
|
||||
}),
|
||||
)
|
||||
});
|
||||
})
|
||||
.child(
|
||||
Disclosure::new(
|
||||
SharedString::from(format!(
|
||||
"terminal-tool-disclosure-{}",
|
||||
terminal.entity_id()
|
||||
)),
|
||||
is_expanded,
|
||||
)
|
||||
.opened_icon(IconName::ChevronUp)
|
||||
.closed_icon(IconName::ChevronDown)
|
||||
.visible_on_hover(&header_group)
|
||||
.on_click(cx.listener({
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
|
||||
let terminal_view = self
|
||||
.entry_view_state
|
||||
@@ -2646,7 +2774,18 @@ impl AcpThreadView {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.text_ui_sm(cx)
|
||||
.children(terminal_view.clone()),
|
||||
.h_full()
|
||||
.children(terminal_view.map(|terminal_view| {
|
||||
if terminal_view
|
||||
.read(cx)
|
||||
.content_mode(window, cx)
|
||||
.is_scrollable()
|
||||
{
|
||||
div().h_72().child(terminal_view).into_any_element()
|
||||
} else {
|
||||
terminal_view.into_any_element()
|
||||
}
|
||||
})),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
@@ -2872,6 +3011,8 @@ impl AcpThreadView {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
@@ -2902,21 +3043,23 @@ impl AcpThreadView {
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(
|
||||
show_description,
|
||||
|el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}. Please choose one of the following options:",
|
||||
self.agent.name()
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -2928,21 +3071,12 @@ impl AcpThreadView {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
)
|
||||
.into_any_element(),
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!connection.auth_methods().is_empty(), |this| {
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
@@ -2954,38 +3088,32 @@ impl AcpThreadView {
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
agent = this.agent.telemetry_id(),
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3250,13 +3378,7 @@ impl AcpThreadView {
|
||||
acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"running",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(delta)))
|
||||
},
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
|
||||
.size(IconSize::Small)
|
||||
@@ -4614,9 +4736,9 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.entry_view_state.update(cx, |entry_view_state, cx| {
|
||||
entry_view_state.settings_changed(cx);
|
||||
entry_view_state.agent_font_size_changed(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4683,6 +4805,7 @@ impl AcpThreadView {
|
||||
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
ThreadError::Refusal => self.render_refusal_error(cx),
|
||||
ThreadError::AuthenticationRequired(error) => {
|
||||
self.render_authentication_required_error(error.clone(), cx)
|
||||
}
|
||||
@@ -4698,6 +4821,75 @@ impl AcpThreadView {
|
||||
Some(div().child(content))
|
||||
}
|
||||
|
||||
fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
|
||||
v_flex().w_full().justify_end().child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.pr_3()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
Icon::new(IconName::Download)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(Label::new("New version available").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
Button::new("update-button", format!("Update to v{}", version))
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_current_model_name(&self, cx: &App) -> SharedString {
|
||||
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
|
||||
// For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
|
||||
// This provides better clarity about what refused the request
|
||||
if self
|
||||
.agent
|
||||
.clone()
|
||||
.downcast::<agent2::NativeAgentServer>()
|
||||
.is_some()
|
||||
{
|
||||
// Native agent - use the model name
|
||||
self.model_selector
|
||||
.as_ref()
|
||||
.and_then(|selector| selector.read(cx).active_model_name(cx))
|
||||
.unwrap_or_else(|| SharedString::from("The model"))
|
||||
} else {
|
||||
// ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
|
||||
self.agent.name()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
|
||||
let model_or_agent_name = self.get_current_model_name(cx);
|
||||
let refusal_message = format!(
|
||||
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
|
||||
model_or_agent_name
|
||||
);
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.title("Request Refused")
|
||||
.icon(IconName::XCircle)
|
||||
.description(refusal_message.clone())
|
||||
.actions_slot(self.create_copy_button(&refusal_message))
|
||||
.dismiss_action(self.dismiss_error_button(cx))
|
||||
}
|
||||
|
||||
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
|
||||
let can_resume = self
|
||||
.thread()
|
||||
@@ -4980,11 +5172,7 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(size)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"load_context_circle",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.with_rotate_animation(3)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -5075,6 +5263,12 @@ impl Render for AcpThreadView {
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
|
||||
}),
|
||||
|this, version| this.child(self.render_new_version_callout(&version, cx)),
|
||||
)
|
||||
.children(
|
||||
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
|
||||
Some(usage_callout.into_any_element())
|
||||
@@ -5329,6 +5523,33 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_refusal_handling(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let (thread_view, cx) =
|
||||
setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
|
||||
|
||||
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||
message_editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text("Do something harmful", window, cx);
|
||||
});
|
||||
|
||||
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||
thread_view.send(window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Check that the refusal error is set
|
||||
thread_view.read_with(cx, |thread_view, _cx| {
|
||||
assert!(
|
||||
matches!(thread_view.thread_error, Some(ThreadError::Refusal)),
|
||||
"Expected refusal error to be set"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -5563,6 +5784,67 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulates a model which always returns a refusal response
|
||||
#[derive(Clone)]
|
||||
struct RefusalAgentConnection;
|
||||
|
||||
impl AgentConnection for RefusalAgentConnection {
|
||||
fn new_thread(
|
||||
self: Rc<Self>,
|
||||
project: Entity<Project>,
|
||||
_cwd: &Path,
|
||||
cx: &mut gpui::App,
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
Task::ready(Ok(cx.new(|cx| {
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
AcpThread::new(
|
||||
"RefusalAgentConnection",
|
||||
self,
|
||||
project,
|
||||
action_log,
|
||||
SessionId("test".into()),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
}
|
||||
|
||||
fn auth_methods(&self) -> &[acp::AuthMethod] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn authenticate(
|
||||
&self,
|
||||
_method_id: acp::AuthMethodId,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
_id: Option<acp_thread::UserMessageId>,
|
||||
_params: acp::PromptRequest,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<acp::PromptResponse>> {
|
||||
Task::ready(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
}))
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -23,9 +23,8 @@ use gpui::{
|
||||
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
|
||||
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
|
||||
ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
|
||||
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
|
||||
UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage,
|
||||
pulsating_between,
|
||||
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle,
|
||||
WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use language_model::{
|
||||
@@ -46,8 +45,8 @@ use std::time::Duration;
|
||||
use text::ToPoint;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
|
||||
Tooltip, prelude::*,
|
||||
Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar,
|
||||
ScrollbarState, TextSize, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
@@ -1002,8 +1001,22 @@ impl ActiveThread {
|
||||
// Don't notify for intermediate tool use
|
||||
}
|
||||
Ok(StopReason::Refusal) => {
|
||||
let model_name = self
|
||||
.thread
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map(|configured| configured.model.name().0.to_string())
|
||||
.unwrap_or_else(|| "The model".to_string());
|
||||
let refusal_message = format!(
|
||||
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
|
||||
model_name
|
||||
);
|
||||
self.last_error = Some(ThreadError::Message {
|
||||
header: SharedString::from("Request Refused"),
|
||||
message: SharedString::from(refusal_message),
|
||||
});
|
||||
self.notify_with_sound(
|
||||
"Language model refused to respond",
|
||||
format!("{} refused to respond", model_name),
|
||||
IconName::Warning,
|
||||
window,
|
||||
cx,
|
||||
@@ -2647,15 +2660,7 @@ impl ActiveThread {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -2831,17 +2836,11 @@ impl ActiveThread {
|
||||
}
|
||||
ToolUseStatus::Pending
|
||||
| ToolUseStatus::InputStillStreaming
|
||||
| ToolUseStatus::Running => {
|
||||
let icon = Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small);
|
||||
icon.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
| ToolUseStatus::Running => Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Accent)
|
||||
.size(IconSize::Small)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
|
||||
ToolUseStatus::Error(_) => {
|
||||
let icon = Icon::new(IconName::Close)
|
||||
@@ -2930,15 +2929,7 @@ impl ActiveThread {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(percentage(
|
||||
delta,
|
||||
)))
|
||||
},
|
||||
),
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(
|
||||
Label::new("Running…")
|
||||
|
||||
@@ -3,9 +3,9 @@ mod configure_context_server_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
@@ -17,10 +17,10 @@ use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
|
||||
WeakEntity, percentage,
|
||||
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Hsla, ScrollHandle, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
@@ -32,8 +32,9 @@ use project::{
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
|
||||
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
|
||||
Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
|
||||
prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
@@ -190,7 +191,7 @@ impl AgentConfiguration {
|
||||
let is_signed_in = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, _| {
|
||||
workspace.client().status().borrow().is_connected()
|
||||
!workspace.client().status().borrow().is_signed_out()
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
@@ -331,6 +332,7 @@ impl AgentConfiguration {
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
@@ -669,10 +671,9 @@ impl AgentConfiguration {
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
SharedString::from(format!("{}-starting", context_server_id.0,)),
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
.with_keyed_rotate_animation(
|
||||
SharedString::from(format!("{}-starting", context_server_id.0)),
|
||||
3,
|
||||
)
|
||||
.into_any_element(),
|
||||
"Server is starting.",
|
||||
@@ -993,15 +994,16 @@ 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
|
||||
.custom
|
||||
.iter()
|
||||
.map(|(name, settings)| {
|
||||
.filter(|(name, _)| *name != "gemini" && *name != "claude")
|
||||
.sorted_by(|(l, _), (r, _)| l.cmp(r))
|
||||
.map(|(name, command)| {
|
||||
self.render_agent_server(
|
||||
IconName::Ai,
|
||||
name.clone(),
|
||||
ExternalAgent::Custom {
|
||||
name: name.clone(),
|
||||
command: settings.command.clone(),
|
||||
command: command.clone(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
@@ -1022,6 +1024,7 @@ impl AgentConfiguration {
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
@@ -1052,7 +1055,7 @@ impl AgentConfiguration {
|
||||
)
|
||||
.child(
|
||||
Label::new(
|
||||
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
|
||||
"All agents connected through the Agent Client Protocol.",
|
||||
)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
@@ -1063,7 +1066,12 @@ impl AgentConfiguration {
|
||||
ExternalAgent::Gemini,
|
||||
cx,
|
||||
))
|
||||
// TODO add CC
|
||||
.child(self.render_agent_server(
|
||||
IconName::AiClaude,
|
||||
"Claude Code",
|
||||
ExternalAgent::ClaudeCode,
|
||||
cx,
|
||||
))
|
||||
.children(user_defined_agents),
|
||||
)
|
||||
}
|
||||
@@ -1093,26 +1101,24 @@ impl AgentConfiguration {
|
||||
.child(Label::new(name.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex().gap_1().child(
|
||||
Button::new(
|
||||
SharedString::from(format!("start_acp_thread-{name}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Thread)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(agent.clone()),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
),
|
||||
Button::new(
|
||||
SharedString::from(format!("start_acp_thread-{name}")),
|
||||
"Start New Thread",
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::Thread)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
NewExternalAgentThread {
|
||||
agent: Some(agent.clone()),
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1276,6 +1282,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
|
||||
let mut unique_server_name = None;
|
||||
// FIXME test that this still works
|
||||
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
|
||||
let server_name: Option<SharedString> = (0..u8::MAX)
|
||||
.map(|i| {
|
||||
@@ -1290,12 +1297,10 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
unique_server_name = Some(server_name.clone());
|
||||
file.custom.insert(
|
||||
server_name,
|
||||
CustomAgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: "path_to_executable".into(),
|
||||
args: vec![],
|
||||
env: Some(HashMap::default()),
|
||||
},
|
||||
AgentServerCommand {
|
||||
path: "path_to_executable".into(),
|
||||
args: vec![],
|
||||
env: Some(HashMap::default()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use context_server::{ContextServerCommand, ContextServerId};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
|
||||
WeakEntity, percentage, prelude::*,
|
||||
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
|
||||
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
@@ -24,7 +22,9 @@ use project::{
|
||||
};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
|
||||
use ui::{
|
||||
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
@@ -638,11 +638,7 @@ impl ConfigureContextServerModal {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -10,12 +10,12 @@ use editor::{
|
||||
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
|
||||
SelectionEffects, ToPoint,
|
||||
actions::{GoToHunk, GoToPreviousHunk},
|
||||
multibuffer_context_lines,
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
|
||||
WeakEntity, Window, percentage, prelude::*,
|
||||
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
|
||||
};
|
||||
|
||||
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
|
||||
@@ -28,9 +28,8 @@ use std::{
|
||||
collections::hash_map::Entry,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
@@ -257,7 +256,7 @@ impl AgentDiffPane {
|
||||
path_key.clone(),
|
||||
buffer.clone(),
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(diff_handle, cx);
|
||||
@@ -1083,11 +1082,7 @@ impl Render for AgentDiffToolbar {
|
||||
Icon::new(IconName::LoadCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent)
|
||||
.with_animation(
|
||||
"load_circle",
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
.with_rotate_animation(3),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
@@ -1522,7 +1517,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
|
||||
@@ -1530,6 +1528,7 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::EntriesRemoved(_)
|
||||
| AcpThreadEvent::ToolAuthorizationRequired
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ use acp_thread::AcpThread;
|
||||
use agent_servers::AgentServerCommand;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::agent::ReauthenticateAgent;
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::agent_diff::AgentDiffThread;
|
||||
use crate::ui::AcpOnboardingModal;
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||
@@ -207,6 +208,9 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| {
|
||||
ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
@@ -284,6 +288,17 @@ impl AgentType {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExternalAgent> for AgentType {
|
||||
fn from(value: ExternalAgent) -> Self {
|
||||
match value {
|
||||
ExternalAgent::Gemini => Self::Gemini,
|
||||
ExternalAgent::ClaudeCode => Self::ClaudeCode,
|
||||
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
|
||||
ExternalAgent::NativeAgent => Self::NativeAgent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveView {
|
||||
pub fn which_font_size_used(&self) -> WhichFontSize {
|
||||
match self {
|
||||
@@ -1049,6 +1064,11 @@ impl AgentPanel {
|
||||
editor
|
||||
});
|
||||
|
||||
if self.selected_agent != AgentType::TextThread {
|
||||
self.selected_agent = AgentType::TextThread;
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::prompt_editor(
|
||||
context_editor.clone(),
|
||||
@@ -1075,6 +1095,7 @@ impl AgentPanel {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let fs = self.fs.clone();
|
||||
let is_not_local = !self.project.read(cx).is_local();
|
||||
|
||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||
|
||||
@@ -1106,17 +1127,21 @@ impl AgentPanel {
|
||||
agent
|
||||
}
|
||||
None => {
|
||||
cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
if is_not_local {
|
||||
ExternalAgent::NativeAgent
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1140,6 +1165,12 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
this.selected_agent = selected_agent;
|
||||
this.serialize(cx);
|
||||
}
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
crate::acp::AcpThreadView::new(
|
||||
server,
|
||||
@@ -1235,6 +1266,12 @@ impl AgentPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if self.selected_agent != AgentType::TextThread {
|
||||
self.selected_agent = AgentType::TextThread;
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
self.set_active_view(
|
||||
ActiveView::prompt_editor(
|
||||
editor,
|
||||
@@ -1860,11 +1897,6 @@ impl AgentPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_agent != agent {
|
||||
self.selected_agent = agent.clone();
|
||||
self.serialize(cx);
|
||||
}
|
||||
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
@@ -1888,13 +1920,17 @@ impl AgentPanel {
|
||||
AgentType::Gemini => {
|
||||
self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
|
||||
}
|
||||
AgentType::ClaudeCode => self.external_thread(
|
||||
Some(crate::ExternalAgent::ClaudeCode),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
AgentType::ClaudeCode => {
|
||||
self.selected_agent = AgentType::ClaudeCode;
|
||||
self.serialize(cx);
|
||||
self.external_thread(
|
||||
Some(crate::ExternalAgent::ClaudeCode),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
AgentType::Custom { name, command } => self.external_thread(
|
||||
Some(crate::ExternalAgent::Custom { name, command }),
|
||||
None,
|
||||
@@ -2500,6 +2536,9 @@ impl AgentPanel {
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
let is_not_local = workspace
|
||||
.update(cx, |workspace, cx| !workspace.project().read(cx).is_local())
|
||||
.unwrap_or_default();
|
||||
|
||||
move |window, cx| {
|
||||
telemetry::event!("New Thread Clicked");
|
||||
@@ -2590,6 +2629,7 @@ impl AgentPanel {
|
||||
ContextMenuEntry::new("New Gemini CLI Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_not_local)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -2616,6 +2656,7 @@ impl AgentPanel {
|
||||
menu.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_not_local)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
@@ -2643,15 +2684,22 @@ impl AgentPanel {
|
||||
// Add custom agents from settings
|
||||
let settings =
|
||||
agent_servers::AllAgentServersSettings::get_global(cx);
|
||||
for (agent_name, agent_settings) in &settings.custom {
|
||||
for (agent_name, command) in
|
||||
settings.iter().sorted_by(|(l, _), (r, _)| l.cmp(r))
|
||||
{
|
||||
if agent_name == "gemini" || agent_name == "claude" {
|
||||
continue;
|
||||
}
|
||||
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new(format!("New {} Thread", agent_name))
|
||||
.icon(IconName::Terminal)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_not_local)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
let agent_name = agent_name.clone();
|
||||
let agent_settings = agent_settings.clone();
|
||||
let command = command.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
@@ -2663,8 +2711,7 @@ impl AgentPanel {
|
||||
AgentType::Custom {
|
||||
name: agent_name
|
||||
.clone(),
|
||||
command: agent_settings
|
||||
.command
|
||||
command: command
|
||||
.clone(),
|
||||
},
|
||||
window,
|
||||
@@ -2926,6 +2973,20 @@ impl AgentPanel {
|
||||
return false;
|
||||
}
|
||||
|
||||
let user_store = self.user_store.read(cx);
|
||||
|
||||
if user_store
|
||||
.plan()
|
||||
.is_some_and(|plan| matches!(plan, Plan::ZedPro))
|
||||
&& user_store
|
||||
.subscription_period()
|
||||
.and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
|
||||
.is_some_and(|date| date < chrono::Utc::now())
|
||||
{
|
||||
OnboardingUpsell::set_dismissed(true, cx);
|
||||
return false;
|
||||
}
|
||||
|
||||
match &self.active_view {
|
||||
ActiveView::History | ActiveView::Configuration => false,
|
||||
ActiveView::ExternalAgentThread { thread_view, .. }
|
||||
@@ -3494,6 +3555,11 @@ impl AgentPanel {
|
||||
) -> AnyElement {
|
||||
let message_with_header = format!("{}\n{}", header, message);
|
||||
|
||||
// Don't show Retry button for refusals
|
||||
let is_refusal = header == "Request Refused";
|
||||
let retry_button = self.render_retry_button(thread);
|
||||
let copy_button = self.create_copy_button(message_with_header);
|
||||
|
||||
Callout::new()
|
||||
.severity(Severity::Error)
|
||||
.icon(IconName::XCircle)
|
||||
@@ -3502,8 +3568,8 @@ impl AgentPanel {
|
||||
.actions_slot(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(self.render_retry_button(thread))
|
||||
.child(self.create_copy_button(message_with_header)),
|
||||
.when(!is_refusal, |this| this.child(retry_button))
|
||||
.child(copy_button),
|
||||
)
|
||||
.dismiss_action(self.dismiss_error_button(thread, cx))
|
||||
.into_any_element()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
@@ -897,6 +900,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,
|
||||
|
||||
@@ -144,7 +144,8 @@ impl InlineAssistant {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return;
|
||||
};
|
||||
let enabled = AgentSettings::get_global(cx).enabled;
|
||||
let enabled = !DisableAiSettings::get_global(cx).disable_ai
|
||||
&& AgentSettings::get_global(cx).enabled;
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
terminal_panel.set_assistant_enabled(enabled, cx)
|
||||
});
|
||||
|
||||
@@ -93,8 +93,8 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
};
|
||||
|
||||
let bottom_padding = match &self.mode {
|
||||
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
|
||||
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
|
||||
PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
|
||||
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
|
||||
};
|
||||
|
||||
buttons.extend(self.render_buttons(window, cx));
|
||||
@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorEvent::Edited { .. } => {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
|
||||
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
|
||||
|
||||
workspace
|
||||
.client()
|
||||
@@ -762,20 +762,22 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
|
||||
fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.bg(colors.editor_background)
|
||||
.child({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_size = settings.buffer_font_size(cx);
|
||||
let line_height = font_size * 1.2;
|
||||
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().editor_foreground,
|
||||
color: colors.editor_foreground,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: font_size.into(),
|
||||
@@ -786,7 +788,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
EditorElement::new(
|
||||
&self.editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
background: colors.editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
|
||||
@@ -7,7 +7,10 @@ use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
|
||||
use language::{Anchor, Buffer, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
|
||||
use project::{
|
||||
CompletionDisplayOptions, CompletionIntent, CompletionSource,
|
||||
lsp_store::CompletionDocumentation,
|
||||
};
|
||||
use rope::Point;
|
||||
use std::{
|
||||
ops::Range,
|
||||
@@ -133,6 +136,7 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
vec![project::CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]
|
||||
})
|
||||
@@ -237,6 +241,7 @@ impl SlashCommandCompletionProvider {
|
||||
|
||||
Ok(vec![project::CompletionResponse {
|
||||
completions,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
// TODO: Could have slash commands indicate whether their completions are incomplete.
|
||||
is_incomplete: true,
|
||||
}])
|
||||
@@ -244,6 +249,7 @@ impl SlashCommandCompletionProvider {
|
||||
} else {
|
||||
Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: true,
|
||||
}]))
|
||||
}
|
||||
@@ -305,6 +311,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
else {
|
||||
return Task::ready(Ok(vec![project::CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]));
|
||||
};
|
||||
|
||||
@@ -2,10 +2,11 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
/// Settings for slash commands.
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "slash_commands")]
|
||||
pub struct SlashCommandSettings {
|
||||
/// Settings for the `/cargo-workspace` slash command.
|
||||
#[serde(default)]
|
||||
@@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
|
||||
}
|
||||
|
||||
impl Settings for SlashCommandSettings {
|
||||
const KEY: Option<&'static str> = Some("slash_commands");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -25,8 +25,8 @@ use gpui::{
|
||||
Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
|
||||
Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
|
||||
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
|
||||
div, img, percentage, point, prelude::*, pulsating_between, size,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
|
||||
prelude::*, pulsating_between, size,
|
||||
};
|
||||
use language::{
|
||||
BufferSnapshot, LspAdapterDelegate, ToOffset,
|
||||
@@ -53,8 +53,8 @@ use std::{
|
||||
};
|
||||
use text::SelectionGoal;
|
||||
use ui::{
|
||||
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
|
||||
prelude::*,
|
||||
ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
||||
TintColor, Tooltip, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, maybe};
|
||||
use workspace::{
|
||||
@@ -1061,15 +1061,7 @@ impl TextThreadEditor {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| {
|
||||
icon.transform(Transformation::rotate(
|
||||
percentage(delta),
|
||||
))
|
||||
},
|
||||
)
|
||||
.with_rotate_animation(2)
|
||||
.into_any_element(),
|
||||
);
|
||||
note = Some(Self::esc_kbd(cx).into_any_element());
|
||||
@@ -2790,11 +2782,7 @@ fn invoked_slash_command_fold_placeholder(
|
||||
.child(Label::new(format!("/{}", command.name)))
|
||||
.map(|parent| match &command.status {
|
||||
InvokedSlashCommandStatus::Running(_) => {
|
||||
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
))
|
||||
parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
|
||||
}
|
||||
InvokedSlashCommandStatus::Error(message) => parent.child(
|
||||
Label::new(format!("error: {message}"))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod acp_onboarding_modal;
|
||||
mod agent_notification;
|
||||
mod burn_mode_tooltip;
|
||||
mod claude_code_onboarding_modal;
|
||||
mod context_pill;
|
||||
mod end_trial_upsell;
|
||||
mod onboarding_modal;
|
||||
@@ -10,6 +11,7 @@ mod unavailable_editing_tooltip;
|
||||
pub use acp_onboarding_modal::*;
|
||||
pub use agent_notification::*;
|
||||
pub use burn_mode_tooltip::*;
|
||||
pub use claude_code_onboarding_modal::*;
|
||||
pub use context_pill::*;
|
||||
pub use end_trial_upsell::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
@@ -141,20 +141,12 @@ impl Render for AcpOnboardingModal {
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(
|
||||
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(111.),
|
||||
rems_from_px(41.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
),
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(257.),
|
||||
rems_from_px(47.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
@@ -62,6 +62,8 @@ impl AgentNotification {
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: None,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
tabbing_identifier: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
254
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs
Normal file
254
crates/agent_ui/src/ui/claude_code_onboarding_modal.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use client::zed_urls;
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
linear_color_stop, linear_gradient,
|
||||
};
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::agent_panel::{AgentPanel, AgentType};
|
||||
|
||||
macro_rules! claude_code_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "ACP Claude Code Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
pub struct ClaudeCodeOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl ClaudeCodeOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_agent_thread(AgentType::ClaudeCode, window, cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
claude_code_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url(&zed_urls::external_agents_docs(cx));
|
||||
cx.notify();
|
||||
|
||||
claude_code_onboarding_event!("Documentation Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ClaudeCodeOnboardingModal {}
|
||||
|
||||
impl Focusable for ClaudeCodeOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for ClaudeCodeOnboardingModal {}
|
||||
|
||||
impl Render for ClaudeCodeOnboardingModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let illustration_element = |icon: IconName, label: Option<SharedString>, opacity: f32| {
|
||||
h_flex()
|
||||
.px_1()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.05))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_dashed()
|
||||
.child(
|
||||
Icon::new(icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
|
||||
)
|
||||
.map(|this| {
|
||||
if let Some(label_text) = label {
|
||||
this.child(
|
||||
Label::new(label_text)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div().w_16().h_1().rounded_full().bg(cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_active
|
||||
.opacity(0.6)),
|
||||
)
|
||||
}
|
||||
})
|
||||
.opacity(opacity)
|
||||
};
|
||||
|
||||
let illustration = h_flex()
|
||||
.relative()
|
||||
.h(rems_from_px(126.))
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_center()
|
||||
.gap_8()
|
||||
.rounded_t_md()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
|
||||
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
|
||||
),
|
||||
)
|
||||
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
|
||||
0.,
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.1),
|
||||
0.9,
|
||||
),
|
||||
linear_color_stop(
|
||||
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||
0.,
|
||||
),
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.size_full()
|
||||
.bg(gpui::black().opacity(0.15)),
|
||||
)
|
||||
.child(
|
||||
Vector::new(
|
||||
VectorName::AcpLogoSerif,
|
||||
rems_from_px(257.),
|
||||
rems_from_px(47.),
|
||||
)
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1p5()
|
||||
.child(illustration_element(IconName::Stop, None, 0.15))
|
||||
.child(illustration_element(
|
||||
IconName::AiGemini,
|
||||
Some("New Gemini CLI Thread".into()),
|
||||
0.3,
|
||||
))
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1()
|
||||
.pr_2()
|
||||
.py_0p5()
|
||||
.gap_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::AiClaude)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Label::new("New Claude Code Thread").size(LabelSize::Small)),
|
||||
)
|
||||
.child(illustration_element(
|
||||
IconName::Stop,
|
||||
Some("Your Agent Here".into()),
|
||||
0.3,
|
||||
))
|
||||
.child(illustration_element(IconName::Stop, None, 0.15)),
|
||||
);
|
||||
|
||||
let heading = v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Beta Release")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Claude Code: Natively in Zed").size(HeadlineSize::Large));
|
||||
|
||||
let copy = "Powered by the Agent Client Protocol, you can now run Claude Code as\na first-class citizen in Zed's agent panel.";
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Start with Claude Code")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let docs_button = Button::new("add-other-agents", "Add Other Agents")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_docs));
|
||||
|
||||
let close_button = h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.id("acp-onboarding")
|
||||
.key_context("AcpOnboardingModal")
|
||||
.relative()
|
||||
.w(rems(34.))
|
||||
.h_full()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
claude_code_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(illustration)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(heading)
|
||||
.child(Label::new(copy).color(Color::Muted))
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.child(open_panel_button)
|
||||
.child(docs_button),
|
||||
),
|
||||
)
|
||||
.child(close_button)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use cloud_llm_client::Plan;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation,
|
||||
Window, percentage,
|
||||
};
|
||||
use ui::{Divider, Vector, VectorName, prelude::*};
|
||||
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
|
||||
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
|
||||
|
||||
use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct AiUpsellCard {
|
||||
pub sign_in_status: SignInStatus,
|
||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
pub account_too_young: bool,
|
||||
pub user_plan: Option<Plan>,
|
||||
pub tab_index: Option<isize>,
|
||||
sign_in_status: SignInStatus,
|
||||
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||
account_too_young: bool,
|
||||
user_plan: Option<Plan>,
|
||||
tab_index: Option<isize>,
|
||||
}
|
||||
|
||||
impl AiUpsellCard {
|
||||
@@ -43,6 +40,11 @@ impl AiUpsellCard {
|
||||
tab_index: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
|
||||
self.tab_index = tab_index;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AiUpsellCard {
|
||||
@@ -84,10 +86,16 @@ impl RenderOnce for AiUpsellCard {
|
||||
)
|
||||
.child(plan_definitions.free_plan());
|
||||
|
||||
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
|
||||
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
|
||||
);
|
||||
let grid_bg = h_flex()
|
||||
.absolute()
|
||||
.inset_0()
|
||||
.w_full()
|
||||
.h(px(240.))
|
||||
.bg(gpui::pattern_slash(
|
||||
cx.theme().colors().border.opacity(0.1),
|
||||
2.,
|
||||
25.,
|
||||
));
|
||||
|
||||
let gradient_bg = div()
|
||||
.absolute()
|
||||
@@ -142,11 +150,7 @@ impl RenderOnce for AiUpsellCard {
|
||||
rems_from_px(72.),
|
||||
)
|
||||
.color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
|
||||
.with_animation(
|
||||
"loading_stamp",
|
||||
Animation::new(Duration::from_secs(10)).repeat(),
|
||||
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
.with_rotate_animation(10),
|
||||
);
|
||||
|
||||
let pro_trial_stamp = div()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::{Client, UserStore};
|
||||
use cloud_llm_client::Plan;
|
||||
use gpui::{Entity, IntoElement, ParentElement};
|
||||
use ui::prelude::*;
|
||||
|
||||
@@ -35,6 +36,8 @@ impl EditPredictionOnboarding {
|
||||
|
||||
impl Render for EditPredictionOnboarding {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let is_free_plan = self.user_store.read(cx).plan() == Some(Plan::ZedFree);
|
||||
|
||||
let github_copilot = v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(if self.copilot_is_configured {
|
||||
@@ -67,7 +70,8 @@ impl Render for EditPredictionOnboarding {
|
||||
self.continue_with_zed_ai.clone(),
|
||||
cx,
|
||||
))
|
||||
.child(ui::Divider::horizontal())
|
||||
.child(github_copilot)
|
||||
.when(is_free_plan, |this| {
|
||||
this.child(ui::Divider::horizontal()).child(github_copilot)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
|
||||
|
||||
impl RenderOnce for YoungAccountBanner {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev.";
|
||||
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. To request an exception, reach out to billing-support@zed.dev.";
|
||||
|
||||
let label = div()
|
||||
.w_full()
|
||||
|
||||
@@ -363,17 +363,15 @@ pub async fn complete(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
) -> Result<Response, AnthropicError> {
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let beta_headers = Model::from_id(&request.model)
|
||||
.map(|model| model.beta_headers())
|
||||
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("Anthropic-Beta", beta_headers)
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let serialized_request =
|
||||
@@ -409,8 +407,9 @@ pub async fn stream_completion(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request)
|
||||
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
|
||||
.await
|
||||
.map(|output| output.0)
|
||||
}
|
||||
@@ -506,6 +505,7 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: Request,
|
||||
beta_headers: String,
|
||||
) -> Result<
|
||||
(
|
||||
BoxStream<'static, Result<Event, AnthropicError>>,
|
||||
@@ -518,15 +518,13 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
stream: true,
|
||||
};
|
||||
let uri = format!("{api_url}/v1/messages");
|
||||
let beta_headers = Model::from_id(&request.base.model)
|
||||
.map(|model| model.beta_headers())
|
||||
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Anthropic-Version", "2023-06-01")
|
||||
.header("Anthropic-Beta", beta_headers)
|
||||
.header("X-Api-Key", api_key)
|
||||
.header("X-Api-Key", api_key.trim())
|
||||
.header("Content-Type", "application/json");
|
||||
let serialized_request =
|
||||
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
|
||||
|
||||
@@ -50,8 +50,9 @@ text.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_env_vars.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
|
||||
let mut contexts = Vec::new();
|
||||
|
||||
let num_peers = rng.gen_range(min_peers..=max_peers);
|
||||
let num_peers = rng.random_range(min_peers..=max_peers);
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
@@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|| !network.lock().is_idle()
|
||||
|| network.lock().contains_disconnected_peers()
|
||||
{
|
||||
let context_index = rng.gen_range(0..contexts.len());
|
||||
let context_index = rng.random_range(0..contexts.len());
|
||||
let context = &contexts[context_index];
|
||||
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0..=29 if mutation_count > 0 => {
|
||||
log::info!("Context {}: edit buffer", context_index);
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
merge_same_roles: true,
|
||||
})];
|
||||
|
||||
let num_sections = rng.gen_range(0..=3);
|
||||
let num_sections = rng.random_range(0..=3);
|
||||
let mut section_start = 0;
|
||||
for _ in 0..num_sections {
|
||||
let mut section_end = rng.gen_range(section_start..=output_text.len());
|
||||
let mut section_end = rng.random_range(section_start..=output_text.len());
|
||||
while !output_text.is_char_boundary(section_end) {
|
||||
section_end += 1;
|
||||
}
|
||||
@@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
75..=84 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let new_status = match rng.gen_range(0..3) {
|
||||
let new_status = match rng.random_range(0..3) {
|
||||
0 => MessageStatus::Done,
|
||||
1 => MessageStatus::Pending,
|
||||
_ => MessageStatus::Error(SharedString::from("Random error")),
|
||||
@@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|
||||
network.lock().broadcast(replica_id, ops_to_send);
|
||||
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
|
||||
} else if rng.gen_bool(0.1) && replica_id != 0 {
|
||||
} else if rng.random_bool(0.1) && replica_id != 0 {
|
||||
log::info!("Context {}: disconnecting", context_index);
|
||||
network.lock().disconnect_peer(replica_id);
|
||||
} else if network.lock().has_unreceived(replica_id) {
|
||||
|
||||
@@ -24,6 +24,7 @@ use rpc::AnyProtoClient;
|
||||
use std::sync::LazyLock;
|
||||
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use zed_env_vars::ZED_STATELESS;
|
||||
|
||||
pub(crate) fn init(client: &AnyProtoClient) {
|
||||
client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
|
||||
@@ -788,8 +789,6 @@ impl ContextStore {
|
||||
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(async move |this, cx| {
|
||||
pub static ZED_STATELESS: LazyLock<bool> =
|
||||
LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
|
||||
if *ZED_STATELESS {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -492,7 +492,7 @@ mod custom_path_matcher {
|
||||
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
|
||||
let globs = globs
|
||||
.iter()
|
||||
.map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
|
||||
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
|
||||
let sources_with_trailing_slash = globs
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Tool for DeletePathTool {
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
fn may_perform_edits(&self) -> bool {
|
||||
|
||||
@@ -1315,17 +1315,17 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_indents(mut rng: StdRng) {
|
||||
let len = rng.gen_range(1..=100);
|
||||
let len = rng.random_range(1..=100);
|
||||
let new_text = util::RandomCharIter::new(&mut rng)
|
||||
.with_simple_text()
|
||||
.take(len)
|
||||
.collect::<String>();
|
||||
let new_text = new_text
|
||||
.split('\n')
|
||||
.map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
|
||||
.map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
|
||||
let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize);
|
||||
|
||||
let chunks = to_random_chunks(&mut rng, &new_text);
|
||||
let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
|
||||
@@ -1357,7 +1357,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -204,7 +204,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -996,7 +996,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -1399,7 +1399,7 @@ fn eval(
|
||||
}
|
||||
|
||||
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy());
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
||||
let mut cx = TestAppContext::build(dispatcher, None);
|
||||
let output = cx.executor().block_test(async {
|
||||
let test = EditAgentTest::new(&mut cx).await;
|
||||
@@ -1707,7 +1707,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
};
|
||||
|
||||
if let Some(retry_after) = retry_delay {
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0));
|
||||
eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}");
|
||||
Timer::after(retry_after + jitter).await;
|
||||
} else {
|
||||
|
||||
@@ -771,7 +771,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -11,11 +11,13 @@ use assistant_tool::{
|
||||
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
||||
use editor::{
|
||||
Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
|
||||
TextStyleRefinement, WeakEntity, pulsating_between, px,
|
||||
};
|
||||
use indoc::formatdoc;
|
||||
use language::{
|
||||
@@ -42,7 +44,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -474,7 +476,7 @@ impl Tool for EditFileTool {
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer,
|
||||
diff_hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff, cx);
|
||||
@@ -703,7 +705,7 @@ impl EditFileToolCard {
|
||||
PathKey::for_buffer(buffer, cx),
|
||||
buffer.clone(),
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
let end = multibuffer.len(cx);
|
||||
@@ -791,7 +793,7 @@ impl EditFileToolCard {
|
||||
path_key,
|
||||
buffer,
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
multibuffer_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff.clone(), cx);
|
||||
@@ -937,11 +939,7 @@ impl ToolCard for EditFileToolCard {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
})
|
||||
.when_some(error_message, |header, error_message| {
|
||||
|
||||
@@ -8,14 +8,14 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
|
||||
TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
|
||||
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use language::LineEnding;
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
use project::{Project, terminals::TerminalKind};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
@@ -28,7 +28,7 @@ use std::{
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
|
||||
use util::{
|
||||
ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
|
||||
time::duration_alt_display,
|
||||
@@ -213,17 +213,16 @@ impl Tool for TerminalTool {
|
||||
async move |cx| {
|
||||
let program = program.await;
|
||||
let env = env.await;
|
||||
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal(
|
||||
TerminalKind::Task(task::SpawnInTerminal {
|
||||
project.create_terminal_task(
|
||||
task::SpawnInTerminal {
|
||||
command: Some(program),
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
@@ -523,11 +522,7 @@ impl ToolCard for TerminalToolCard {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
),
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
})
|
||||
.when(tool_failed || command_failed, |header| {
|
||||
|
||||
@@ -15,9 +15,10 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
settings.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AssetSource, Global};
|
||||
use rodio::{Decoder, Source, source::Buffered};
|
||||
|
||||
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||
|
||||
pub struct SoundRegistry {
|
||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||
assets: Box<dyn AssetSource>,
|
||||
}
|
||||
|
||||
struct GlobalSoundRegistry(Arc<SoundRegistry>);
|
||||
|
||||
impl Global for GlobalSoundRegistry {}
|
||||
|
||||
impl SoundRegistry {
|
||||
pub fn new(source: impl AssetSource) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
cache: Default::default(),
|
||||
assets: Box::new(source),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn global(cx: &App) -> Arc<Self> {
|
||||
cx.global::<GlobalSoundRegistry>().0.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
|
||||
cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
|
||||
if let Some(wav) = self.cache.lock().get(name) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", name);
|
||||
let bytes = self
|
||||
.assets
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.cache.lock().insert(name.to_string(), source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
use assets::SoundRegistry;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||
use rodio::{OutputStream, OutputStreamBuilder};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, BorrowAppContext, Global};
|
||||
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
|
||||
use settings::Settings;
|
||||
use std::io::Cursor;
|
||||
use util::ResultExt;
|
||||
|
||||
mod assets;
|
||||
mod audio_settings;
|
||||
pub use audio_settings::AudioSettings;
|
||||
|
||||
pub fn init(source: impl AssetSource, cx: &mut App) {
|
||||
SoundRegistry::set_global(source, cx);
|
||||
cx.set_global(GlobalAudio(Audio::new()));
|
||||
pub fn init(cx: &mut App) {
|
||||
AudioSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
|
||||
pub enum Sound {
|
||||
Joined,
|
||||
Leave,
|
||||
@@ -38,18 +41,12 @@ impl Sound {
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
output_handle: Option<OutputStream>,
|
||||
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
struct GlobalAudio(Audio);
|
||||
|
||||
impl Global for GlobalAudio {}
|
||||
impl Global for Audio {}
|
||||
|
||||
impl Audio {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
if self.output_handle.is_none() {
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
@@ -58,26 +55,51 @@ impl Audio {
|
||||
self.output_handle.as_ref()
|
||||
}
|
||||
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
if !cx.has_global::<GlobalAudio>() {
|
||||
return;
|
||||
}
|
||||
pub fn play_source(
|
||||
source: impl rodio::Source + Send + 'static,
|
||||
cx: &mut App,
|
||||
) -> anyhow::Result<()> {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
let output_handle = this
|
||||
.ensure_output_exists()
|
||||
.ok_or_else(|| anyhow!("Could not open audio output"))?;
|
||||
output_handle.mixer().add(source);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||
pub fn play_sound(sound: Sound, cx: &mut App) {
|
||||
cx.update_default_global(|this: &mut Self, cx| {
|
||||
let source = this.sound_source(sound, cx).log_err()?;
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.mixer().add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn end_call(cx: &mut App) {
|
||||
if !cx.has_global::<GlobalAudio>() {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||
cx.update_default_global(|this: &mut Self, _cx| {
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
|
||||
fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
|
||||
if let Some(wav) = self.source_cache.get(&sound) {
|
||||
return Ok(wav.clone());
|
||||
}
|
||||
|
||||
let path = format!("sounds/{}.wav", sound.file());
|
||||
let bytes = cx
|
||||
.asset_source()
|
||||
.load(&path)?
|
||||
.map(anyhow::Ok)
|
||||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.source_cache.insert(sound, source.clone());
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
}
|
||||
|
||||
32
crates/audio/src/audio_settings.rs
Normal file
32
crates/audio/src/audio_settings.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct AudioSettings {
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool, // default is false
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[serde(default)]
|
||||
#[settings_key(key = "audio")]
|
||||
pub struct AudioSettingsContent {
|
||||
/// Whether to use the experimental audio system
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
pub rodio_audio: bool,
|
||||
}
|
||||
|
||||
impl Settings for AudioSettings {
|
||||
type FileContent = AudioSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{
|
||||
@@ -118,14 +118,13 @@ struct AutoUpdateSetting(bool);
|
||||
/// Whether or not to automatically check for updates.
|
||||
///
|
||||
/// Default: true
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
|
||||
#[serde(transparent)]
|
||||
#[settings_key(key = "auto_update")]
|
||||
struct AutoUpdateSettingContent(bool);
|
||||
|
||||
impl Settings for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = Option<AutoUpdateSettingContent>;
|
||||
type FileContent = AutoUpdateSettingContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let auto_update = [
|
||||
@@ -135,17 +134,19 @@ impl Settings for AutoUpdateSetting {
|
||||
sources.user,
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|value| value.copied().flatten())
|
||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
|
||||
.find_map(|value| value.copied())
|
||||
.unwrap_or(*sources.default);
|
||||
|
||||
Ok(Self(auto_update.0))
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
vscode.enum_setting("update.mode", current, |s| match s {
|
||||
let mut cur = &mut Some(*current);
|
||||
vscode.enum_setting("update.mode", &mut cur, |s| match s {
|
||||
"none" | "manual" => Some(AutoUpdateSettingContent(false)),
|
||||
_ => Some(AutoUpdateSettingContent(true)),
|
||||
});
|
||||
*current = cur.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED;
|
||||
type Job = fn(&Path) -> Result<()>;
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) const JOBS: [Job; 6] = [
|
||||
pub(crate) const JOBS: &[Job] = &[
|
||||
// Delete old files
|
||||
|app_dir| {
|
||||
let zed_executable = app_dir.join("Zed.exe");
|
||||
@@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [
|
||||
std::fs::remove_file(&zed_cli)
|
||||
.context(format!("Failed to remove old file {}", zed_cli.display()))
|
||||
},
|
||||
|app_dir| {
|
||||
let zed_wsl = app_dir.join("bin\\zed");
|
||||
log::info!("Removing old file: {}", zed_wsl.display());
|
||||
std::fs::remove_file(&zed_wsl)
|
||||
.context(format!("Failed to remove old file {}", zed_wsl.display()))
|
||||
},
|
||||
// Copy new files
|
||||
|app_dir| {
|
||||
let zed_executable_source = app_dir.join("install\\Zed.exe");
|
||||
@@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [
|
||||
zed_cli_dest.display()
|
||||
))
|
||||
},
|
||||
|app_dir| {
|
||||
let zed_wsl_source = app_dir.join("install\\bin\\zed");
|
||||
let zed_wsl_dest = app_dir.join("bin\\zed");
|
||||
log::info!(
|
||||
"Copying new file {} to {}",
|
||||
zed_wsl_source.display(),
|
||||
zed_wsl_dest.display()
|
||||
);
|
||||
std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
|
||||
.map(|_| ())
|
||||
.context(format!(
|
||||
"Failed to copy new file {} to {}",
|
||||
zed_wsl_source.display(),
|
||||
zed_wsl_dest.display()
|
||||
))
|
||||
},
|
||||
// Clean up installer folder and updates folder
|
||||
|app_dir| {
|
||||
let updates_folder = app_dir.join("updates");
|
||||
@@ -85,7 +107,7 @@ pub(crate) const JOBS: [Job; 6] = [
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const JOBS: [Job; 2] = [
|
||||
pub(crate) const JOBS: &[Job] = &[
|
||||
|_| {
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {
|
||||
|
||||
@@ -2044,10 +2044,10 @@ mod tests {
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
"\n".to_owned()
|
||||
} else {
|
||||
let c = rng.gen_range('A'..='Z');
|
||||
let c = rng.random_range('A'..='Z');
|
||||
format!("{c}{c}{c}\n")
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2066,7 @@ mod tests {
|
||||
old_lines.into_iter()
|
||||
};
|
||||
let mut result = String::new();
|
||||
let unchanged_count = rng.gen_range(0..=old_lines.len());
|
||||
let unchanged_count = rng.random_range(0..=old_lines.len());
|
||||
result +=
|
||||
&old_lines
|
||||
.by_ref()
|
||||
@@ -2076,14 +2076,14 @@ mod tests {
|
||||
s
|
||||
});
|
||||
while old_lines.len() > 0 {
|
||||
let deleted_count = rng.gen_range(0..=old_lines.len());
|
||||
let deleted_count = rng.random_range(0..=old_lines.len());
|
||||
let _advance = old_lines
|
||||
.by_ref()
|
||||
.take(deleted_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.gen_range(minimum_added..=5);
|
||||
let added_count = rng.random_range(minimum_added..=5);
|
||||
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
|
||||
result += &addition;
|
||||
|
||||
@@ -2092,7 +2092,8 @@ mod tests {
|
||||
if blank_lines == old_lines.len() {
|
||||
break;
|
||||
};
|
||||
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
let unchanged_count =
|
||||
rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
result += &old_lines.by_ref().take(unchanged_count).fold(
|
||||
String::new(),
|
||||
|mut s, line| {
|
||||
@@ -2149,7 +2150,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
|
||||
let mut index_text = if rng.r#gen() {
|
||||
let mut index_text = if rng.random() {
|
||||
Rope::from(head_text.as_str())
|
||||
} else {
|
||||
working_copy.as_rope().clone()
|
||||
@@ -2165,7 +2166,7 @@ mod tests {
|
||||
}
|
||||
|
||||
for _ in 0..operations {
|
||||
let i = rng.gen_range(0..hunks.len());
|
||||
let i = rng.random_range(0..hunks.len());
|
||||
let hunk = &mut hunks[i];
|
||||
let hunk_to_change = hunk.clone();
|
||||
let stage = match hunk.secondary_status {
|
||||
|
||||
@@ -1161,7 +1161,7 @@ impl Room {
|
||||
let request = self.client.request(proto::ShareProject {
|
||||
room_id: self.id(),
|
||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||
is_ssh_project: project.read(cx).is_via_ssh(),
|
||||
is_ssh_project: project.read(cx).is_via_remote_server(),
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
@@ -11,7 +11,8 @@ pub struct CallSettings {
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "calls")]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
const KEY: Option<&'static str> = Some("calls");
|
||||
|
||||
type FileContent = CallSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user