Compare commits
203 Commits
v0.202.3-p
...
make-langu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a46eb6737c | ||
|
|
d80fa82009 | ||
|
|
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 |
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -81,6 +81,7 @@ jobs:
|
||||
echo "run_license=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
|
||||
echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
|
||||
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
|
||||
echo "run_nix=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
@@ -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.**
|
||||
|
||||
203
Cargo.lock
generated
203
Cargo.lock
generated
@@ -23,6 +23,7 @@ dependencies = [
|
||||
"language_model",
|
||||
"markdown",
|
||||
"parking_lot",
|
||||
"portable-pty",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
@@ -30,6 +31,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"task",
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"ui",
|
||||
@@ -37,6 +39,7 @@ dependencies = [
|
||||
"util",
|
||||
"uuid",
|
||||
"watch",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -507,7 +510,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"piper",
|
||||
"polling",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata",
|
||||
"rustix-openpty",
|
||||
"serde",
|
||||
"signal-hook",
|
||||
@@ -847,6 +850,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2457,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",
|
||||
]
|
||||
|
||||
@@ -4488,6 +4492,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_env_vars",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4732,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]]
|
||||
@@ -5631,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]]
|
||||
@@ -5642,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]]
|
||||
@@ -7293,8 +7298,8 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8299,7 +8304,7 @@ dependencies = [
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
@@ -8898,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",
|
||||
@@ -8951,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"
|
||||
@@ -9108,6 +9151,7 @@ dependencies = [
|
||||
"icons",
|
||||
"image",
|
||||
"log",
|
||||
"open_router",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"schemars",
|
||||
@@ -9211,6 +9255,7 @@ dependencies = [
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"proto",
|
||||
"release_channel",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -9699,7 +9744,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
"rustc_version",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
@@ -9771,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",
|
||||
@@ -9914,9 +9959,11 @@ dependencies = [
|
||||
"editor",
|
||||
"fs",
|
||||
"gpui",
|
||||
"html5ever 0.27.0",
|
||||
"language",
|
||||
"linkify",
|
||||
"log",
|
||||
"markup5ever_rcdom",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark 0.12.2",
|
||||
"settings",
|
||||
@@ -9977,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]]
|
||||
@@ -10682,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"
|
||||
@@ -11190,6 +11227,8 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -11385,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"
|
||||
@@ -13381,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]]
|
||||
@@ -13402,7 +13426,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13411,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"
|
||||
@@ -13734,7 +13752,6 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
|
||||
"serde",
|
||||
"smol",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -14855,6 +14872,8 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"serde_path_to_error",
|
||||
"settings_ui_macros",
|
||||
"smallvec",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json",
|
||||
@@ -14890,39 +14909,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]]
|
||||
@@ -16738,6 +16748,7 @@ dependencies = [
|
||||
"db",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"keymap_editor",
|
||||
"notifications",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
@@ -16746,7 +16757,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"settings_ui",
|
||||
"smallvec",
|
||||
"story",
|
||||
"telemetry",
|
||||
@@ -17115,14 +17125,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",
|
||||
@@ -17153,7 +17163,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
"regex-syntax 0.8.5",
|
||||
"regex-syntax",
|
||||
"serde_json",
|
||||
"streaming-iterator",
|
||||
"tree-sitter-language",
|
||||
@@ -17183,8 +17193,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",
|
||||
@@ -19952,8 +19961,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",
|
||||
@@ -20135,9 +20144,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",
|
||||
@@ -20394,7 +20403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.202.3"
|
||||
version = "0.204.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
@@ -20458,6 +20467,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"jj_ui",
|
||||
"journal",
|
||||
"keymap_editor",
|
||||
"language",
|
||||
"language_extension",
|
||||
"language_model",
|
||||
@@ -20543,6 +20553,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_env_vars",
|
||||
"zeta",
|
||||
"zlog",
|
||||
"zlog_settings",
|
||||
@@ -20559,6 +20570,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"
|
||||
@@ -20588,7 +20606,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_html"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
@@ -20787,6 +20805,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"http_client",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
@@ -20801,6 +20820,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"strum 0.27.1",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"theme",
|
||||
@@ -20808,7 +20828,6 @@ dependencies = [
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace",
|
||||
|
||||
27
Cargo.toml
27
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",
|
||||
@@ -146,6 +147,7 @@ members = [
|
||||
"crates/settings",
|
||||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/settings_ui_macros",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
@@ -156,9 +158,9 @@ members = [
|
||||
"crates/streaming_diff",
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/system_specs",
|
||||
"crates/supermaven_api",
|
||||
"crates/svg_preview",
|
||||
"crates/system_specs",
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
@@ -191,6 +193,7 @@ members = [
|
||||
"crates/x_ai",
|
||||
"crates/zed",
|
||||
"crates/zed_actions",
|
||||
"crates/zed_env_vars",
|
||||
"crates/zeta",
|
||||
"crates/zeta_cli",
|
||||
"crates/zlog",
|
||||
@@ -297,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" }
|
||||
@@ -314,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" }
|
||||
@@ -373,6 +375,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" }
|
||||
@@ -418,6 +421,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" }
|
||||
@@ -426,7 +430,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"
|
||||
@@ -519,7 +523,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"
|
||||
@@ -588,6 +592,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"
|
||||
@@ -624,7 +629,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"
|
||||
@@ -691,6 +696,7 @@ features = [
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
"Win32_Graphics_Hlsl",
|
||||
"Win32_Networking_WinSock",
|
||||
"Win32_Security",
|
||||
"Win32_Security_Credentials",
|
||||
@@ -842,6 +848,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",
|
||||
|
||||
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 |
@@ -63,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",
|
||||
@@ -130,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",
|
||||
@@ -170,6 +170,7 @@
|
||||
"context": "Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-insert": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
@@ -258,6 +259,7 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-insert": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -66,8 +66,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",
|
||||
@@ -134,8 +134,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",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"alt-shift-right": "editor::SelectToNextSubwordEnd",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"alt-,": "pane::GoBack", // xref-pop-marker-stack
|
||||
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
|
||||
"ctrl-d": "editor::Delete", // delete-char
|
||||
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
|
||||
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
|
||||
"ctrl-k": "editor::KillRingCut", // kill-line
|
||||
"ctrl-w": "editor::Cut", // kill-region
|
||||
"alt-w": "editor::Copy", // kill-ring-save
|
||||
|
||||
@@ -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": {
|
||||
@@ -435,7 +444,7 @@
|
||||
"g b": "vim::WindowBottom",
|
||||
|
||||
"shift-r": "editor::Paste",
|
||||
"x": "editor::SelectLine",
|
||||
"x": "vim::HelixSelectLine",
|
||||
"shift-x": "editor::SelectLine",
|
||||
"%": "editor::SelectAll",
|
||||
// Window mode
|
||||
|
||||
@@ -172,7 +172,7 @@ The user has specified the following rules that should be applied:
|
||||
Rules title: {{title}}
|
||||
{{/if}}
|
||||
``````
|
||||
{{contents}}}
|
||||
{{contents}}
|
||||
``````
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
@@ -188,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:
|
||||
@@ -223,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,
|
||||
@@ -266,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
|
||||
@@ -279,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
|
||||
@@ -363,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.
|
||||
@@ -1756,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"
|
||||
@@ -1904,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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -405,13 +407,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 +429,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 +452,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 +659,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 +717,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;
|
||||
|
||||
@@ -484,11 +485,15 @@ impl AgentMessage {
|
||||
};
|
||||
|
||||
for tool_result in self.tool_results.values() {
|
||||
let mut tool_result = tool_result.clone();
|
||||
// Surprisingly, the API fails if we return an empty string here.
|
||||
// It thinks we are sending a tool use without a tool result.
|
||||
if tool_result.content.is_empty() {
|
||||
tool_result.content = "<Tool returned an empty string>".into();
|
||||
}
|
||||
user_message
|
||||
.content
|
||||
.push(language_model::MessageContent::ToolResult(
|
||||
tool_result.clone(),
|
||||
));
|
||||
.push(language_model::MessageContent::ToolResult(tool_result));
|
||||
}
|
||||
|
||||
let mut messages = Vec::new();
|
||||
@@ -519,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),
|
||||
@@ -531,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,
|
||||
@@ -1020,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()));
|
||||
@@ -1041,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);
|
||||
}
|
||||
@@ -2385,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(()));
|
||||
|
||||
@@ -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> {
|
||||
@@ -73,6 +82,7 @@ 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 {
|
||||
@@ -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(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,7 +40,7 @@ impl ClaudeCode {
|
||||
Self::PACKAGE_NAME.into(),
|
||||
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
|
||||
true,
|
||||
None,
|
||||
Some("0.2.5".parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
@@ -76,6 +76,7 @@ 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()
|
||||
@@ -109,6 +110,13 @@ impl AgentServer for ClaudeCode {
|
||||
.insert("ANTHROPIC_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()
|
||||
);
|
||||
|
||||
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -498,7 +498,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))
|
||||
|
||||
@@ -36,6 +36,7 @@ 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()
|
||||
@@ -74,6 +75,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 +100,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 +137,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,13 +6,13 @@ use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<CustomAgentServerSettings>,
|
||||
|
||||
@@ -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, SettingsSources, SettingsUi};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
@@ -223,7 +223,7 @@ impl AgentSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -352,18 +352,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"
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -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<Option<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(Some(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<Option<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;
|
||||
@@ -45,8 +45,8 @@ use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::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,7 +286,9 @@ 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],
|
||||
}
|
||||
@@ -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,7 +374,8 @@ impl AcpThreadView {
|
||||
history_store.clone(),
|
||||
prompt_store.clone(),
|
||||
prompt_capabilities.clone(),
|
||||
prevent_slash_commands,
|
||||
available_commands.clone(),
|
||||
agent.name(),
|
||||
)
|
||||
});
|
||||
|
||||
@@ -396,18 +407,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 +443,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 +628,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 +680,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 +886,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 +961,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: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let contents = self
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| message_editor.contents(cx));
|
||||
@@ -1189,6 +1285,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 +1324,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 +1399,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 +1415,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 +1473,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 +1501,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 +1523,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 +2550,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 +2639,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 +2700,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 +2773,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()
|
||||
@@ -2928,16 +3066,7 @@ 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)),
|
||||
)
|
||||
@@ -3250,13 +3379,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)
|
||||
@@ -4683,6 +4806,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 +4822,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 +5173,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 +5264,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 +5524,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 +5785,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,7 +3,7 @@ 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_settings::AgentSettings;
|
||||
@@ -17,9 +17,8 @@ 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 language::LanguageRegistry;
|
||||
use language_model::{
|
||||
@@ -32,8 +31,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};
|
||||
@@ -331,6 +331,7 @@ impl AgentConfiguration {
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
@@ -669,10 +670,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.",
|
||||
@@ -1022,6 +1022,7 @@ impl AgentConfiguration {
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.pr_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
@@ -1052,7 +1053,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 +1064,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 +1099,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,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
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 +207,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 +287,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 +1063,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 +1094,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 +1126,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 +1164,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 +1265,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 +1896,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 +1919,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 +2535,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 +2628,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 +2655,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();
|
||||
@@ -2648,6 +2688,7 @@ impl AgentPanel {
|
||||
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();
|
||||
@@ -2926,6 +2967,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 +3549,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 +3562,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()
|
||||
|
||||
@@ -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));
|
||||
@@ -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,10 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
|
||||
/// Settings for slash commands.
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
|
||||
pub struct SlashCommandSettings {
|
||||
/// Settings for the `/cargo-workspace` slash command.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -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,12 +1,9 @@
|
||||
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};
|
||||
|
||||
@@ -89,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()
|
||||
@@ -147,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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -2,9 +2,9 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AudioSettings {
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
@@ -12,7 +12,7 @@ pub struct AudioSettings {
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[serde(default)]
|
||||
pub struct AudioSettingsContent {
|
||||
/// Whether to use the experimental audio system
|
||||
|
||||
@@ -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, SettingsSources, SettingsStore, SettingsUi};
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{
|
||||
@@ -118,14 +118,14 @@ 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)]
|
||||
#[serde(transparent)]
|
||||
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 +135,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") {
|
||||
|
||||
@@ -3,6 +3,7 @@ mod models;
|
||||
use anyhow::{Context, Error, Result, anyhow};
|
||||
use aws_sdk_bedrockruntime as bedrock;
|
||||
pub use aws_sdk_bedrockruntime as bedrock_client;
|
||||
use aws_sdk_bedrockruntime::types::InferenceConfiguration;
|
||||
pub use aws_sdk_bedrockruntime::types::{
|
||||
AnyToolChoice as BedrockAnyToolChoice, AutoToolChoice as BedrockAutoToolChoice,
|
||||
ContentBlock as BedrockInnerContent, Tool as BedrockTool, ToolChoice as BedrockToolChoice,
|
||||
@@ -17,7 +18,8 @@ pub use bedrock::types::{
|
||||
ConverseOutput as BedrockResponse, ConverseStreamOutput as BedrockStreamingResponse,
|
||||
ImageBlock as BedrockImageBlock, Message as BedrockMessage,
|
||||
ReasoningContentBlock as BedrockThinkingBlock, ReasoningTextBlock as BedrockThinkingTextBlock,
|
||||
ResponseStream as BedrockResponseStream, ToolResultBlock as BedrockToolResultBlock,
|
||||
ResponseStream as BedrockResponseStream, SystemContentBlock as BedrockSystemContentBlock,
|
||||
ToolResultBlock as BedrockToolResultBlock,
|
||||
ToolResultContentBlock as BedrockToolResultContentBlock,
|
||||
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
|
||||
};
|
||||
@@ -58,6 +60,20 @@ pub async fn stream_completion(
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
|
||||
let inference_config = InferenceConfiguration::builder()
|
||||
.max_tokens(request.max_tokens as i32)
|
||||
.set_temperature(request.temperature)
|
||||
.set_top_p(request.top_p)
|
||||
.build();
|
||||
|
||||
response = response.inference_config(inference_config);
|
||||
|
||||
if let Some(system) = request.system {
|
||||
if !system.is_empty() {
|
||||
response = response.system(BedrockSystemContentBlock::Text(system));
|
||||
}
|
||||
}
|
||||
|
||||
let output = response
|
||||
.send()
|
||||
.await
|
||||
|
||||
@@ -151,12 +151,12 @@ impl Model {
|
||||
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Model::ClaudeSonnet4 => "claude-4-sonnet",
|
||||
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
|
||||
Model::ClaudeOpus4 => "claude-4-opus",
|
||||
Model::ClaudeOpus4_1 => "claude-4-opus-1",
|
||||
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
|
||||
Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking",
|
||||
Model::ClaudeSonnet4 => "claude-sonnet-4",
|
||||
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking",
|
||||
Model::ClaudeOpus4 => "claude-opus-4",
|
||||
Model::ClaudeOpus4_1 => "claude-opus-4-1",
|
||||
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
|
||||
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
|
||||
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Model::Claude3Opus => "claude-3-opus",
|
||||
@@ -359,14 +359,12 @@ impl Model {
|
||||
pub fn max_output_tokens(&self) -> u64 {
|
||||
match self {
|
||||
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
|
||||
Self::Claude3_7Sonnet
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeOpus4
|
||||
| Model::ClaudeOpus4Thinking
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
|
||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Model::ClaudeOpus4_1Thinking => 128_000,
|
||||
| Self::ClaudeOpus4_1Thinking => 32_000,
|
||||
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
@@ -784,10 +782,10 @@ mod tests {
|
||||
);
|
||||
|
||||
// Test thinking models have different friendly IDs but same request IDs
|
||||
assert_eq!(Model::ClaudeSonnet4.id(), "claude-4-sonnet");
|
||||
assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4");
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4Thinking.id(),
|
||||
"claude-4-sonnet-thinking"
|
||||
"claude-sonnet-4-thinking"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.request_id(),
|
||||
|
||||
@@ -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, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
@@ -11,7 +11,7 @@ pub struct CallSettings {
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
|
||||
@@ -14,6 +14,7 @@ pub enum CliRequest {
|
||||
paths: Vec<String>,
|
||||
urls: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
wsl: Option<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::Parser;
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
env, fs, io,
|
||||
@@ -85,6 +84,18 @@ struct Args {
|
||||
/// Run zed in dev-server mode
|
||||
#[arg(long)]
|
||||
dev_server_token: Option<String>,
|
||||
/// The username and WSL distribution to use when opening paths. If not specified,
|
||||
/// Zed will attempt to open the paths directly.
|
||||
///
|
||||
/// The username is optional, and if not specified, the default user for the distribution
|
||||
/// will be used.
|
||||
///
|
||||
/// Example: `me@Ubuntu` or `Ubuntu`.
|
||||
///
|
||||
/// WARN: You should not fill in this field by hand.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[arg(long, value_name = "USER@DISTRO")]
|
||||
wsl: Option<String>,
|
||||
/// Not supported in Zed CLI, only supported on Zed binary
|
||||
/// Will attempt to give the correct command to run
|
||||
#[arg(long)]
|
||||
@@ -129,14 +140,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
||||
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(all(not(debug_assertions), target_os = "windows"))]
|
||||
unsafe {
|
||||
use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
|
||||
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
|
||||
let mut command = util::command::new_std_command("wsl.exe");
|
||||
|
||||
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
|
||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||
if user.is_empty() {
|
||||
anyhow::bail!("user is empty in wsl argument");
|
||||
}
|
||||
(Some(user), distro)
|
||||
} else {
|
||||
(None, wsl)
|
||||
};
|
||||
|
||||
if let Some(user) = user {
|
||||
command.arg("--user").arg(user);
|
||||
}
|
||||
|
||||
let output = command
|
||||
.arg("--distribution")
|
||||
.arg(distro_name)
|
||||
.arg("wslpath")
|
||||
.arg("-m")
|
||||
.arg(source)
|
||||
.output()?;
|
||||
|
||||
let result = String::from_utf8_lossy(&output.stdout);
|
||||
let prefix = format!("//wsl.localhost/{}", distro_name);
|
||||
|
||||
Ok(result
|
||||
.trim()
|
||||
.strip_prefix(&prefix)
|
||||
.unwrap_or(&result)
|
||||
.to_string())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
util::prevent_root_execution();
|
||||
|
||||
@@ -223,6 +261,8 @@ fn main() -> Result<()> {
|
||||
let env = {
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
use collections::HashMap;
|
||||
|
||||
// On Linux, the desktop entry uses `cli` to spawn `zed`.
|
||||
// We need to handle env vars correctly since std::env::vars() may not contain
|
||||
// project-specific vars (e.g. those set by direnv).
|
||||
@@ -235,8 +275,19 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
Some(std::env::vars().collect::<HashMap<_, _>>())
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, by default, a child process inherits a copy of the environment block of the parent process.
|
||||
// So we don't need to pass env vars explicitly.
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
|
||||
{
|
||||
use collections::HashMap;
|
||||
|
||||
Some(std::env::vars().collect::<HashMap<_, _>>())
|
||||
}
|
||||
};
|
||||
|
||||
let exit_status = Arc::new(Mutex::new(None));
|
||||
@@ -253,6 +304,11 @@ fn main() -> Result<()> {
|
||||
]);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let wsl = args.wsl.as_ref();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let wsl = None;
|
||||
|
||||
for path in args.paths_with_position.iter() {
|
||||
if path.starts_with("zed://")
|
||||
|| path.starts_with("http://")
|
||||
@@ -271,8 +327,10 @@ fn main() -> Result<()> {
|
||||
paths.push(tmp_file.path().to_string_lossy().to_string());
|
||||
let (tmp_file, _) = tmp_file.keep()?;
|
||||
anonymous_fd_tmp_files.push((file, tmp_file));
|
||||
} else if let Some(wsl) = wsl {
|
||||
urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
|
||||
} else {
|
||||
paths.push(parse_path_with_position(path)?)
|
||||
paths.push(parse_path_with_position(path)?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,10 +346,16 @@ fn main() -> Result<()> {
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
let (tx, rx) = (handshake.requests, handshake.responses);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let wsl = args.wsl;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let wsl = None;
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
wsl,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
env,
|
||||
@@ -644,15 +708,15 @@ mod windows {
|
||||
Storage::FileSystem::{
|
||||
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
|
||||
},
|
||||
System::Threading::CreateMutexW,
|
||||
System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
|
||||
},
|
||||
core::HSTRING,
|
||||
};
|
||||
|
||||
use crate::{Detect, InstalledApp};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitStatus;
|
||||
use std::{io, os::windows::process::CommandExt};
|
||||
|
||||
fn check_single_instance() -> bool {
|
||||
let mutex = unsafe {
|
||||
@@ -691,6 +755,7 @@ mod windows {
|
||||
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
|
||||
if check_single_instance() {
|
||||
std::process::Command::new(self.0.clone())
|
||||
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
|
||||
.arg(ipc_url)
|
||||
.spawn()?;
|
||||
} else {
|
||||
|
||||
@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
convert::TryFrom,
|
||||
@@ -96,7 +96,7 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
pub struct ClientSettingsContent {
|
||||
server_url: Option<String>,
|
||||
}
|
||||
@@ -122,7 +122,7 @@ impl Settings for ClientSettings {
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
pub struct ProxySettingsContent {
|
||||
proxy: Option<String>,
|
||||
}
|
||||
@@ -287,6 +287,7 @@ pub enum Status {
|
||||
},
|
||||
ConnectionLost,
|
||||
Reauthenticating,
|
||||
Reauthenticated,
|
||||
Reconnecting,
|
||||
ReconnectionError {
|
||||
next_reconnection: Instant,
|
||||
@@ -298,6 +299,21 @@ impl Status {
|
||||
matches!(self, Self::Connected { .. })
|
||||
}
|
||||
|
||||
pub fn was_connected(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ConnectionLost
|
||||
| Self::Reauthenticating
|
||||
| Self::Reauthenticated
|
||||
| Self::Reconnecting
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns whether the client is currently connected or was connected at some point.
|
||||
pub fn is_or_was_connected(&self) -> bool {
|
||||
self.is_connected() || self.was_connected()
|
||||
}
|
||||
|
||||
pub fn is_signing_in(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
@@ -511,7 +527,7 @@ pub struct TelemetrySettings {
|
||||
}
|
||||
|
||||
/// Control what info is collected by Zed.
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct TelemetrySettingsContent {
|
||||
/// Send debug info like crash reports.
|
||||
///
|
||||
@@ -857,11 +873,13 @@ impl Client {
|
||||
try_provider: bool,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Credentials> {
|
||||
if self.status().borrow().is_signed_out() {
|
||||
let is_reauthenticating = if self.status().borrow().is_signed_out() {
|
||||
self.set_status(Status::Authenticating, cx);
|
||||
false
|
||||
} else {
|
||||
self.set_status(Status::Reauthenticating, cx);
|
||||
}
|
||||
true
|
||||
};
|
||||
|
||||
let mut credentials = None;
|
||||
|
||||
@@ -919,7 +937,14 @@ impl Client {
|
||||
self.cloud_client
|
||||
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
|
||||
self.state.write().credentials = Some(credentials.clone());
|
||||
self.set_status(Status::Authenticated, cx);
|
||||
self.set_status(
|
||||
if is_reauthenticating {
|
||||
Status::Reauthenticated
|
||||
} else {
|
||||
Status::Authenticated
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
@@ -1034,6 +1059,7 @@ impl Client {
|
||||
| Status::Authenticating
|
||||
| Status::AuthenticationError
|
||||
| Status::Reauthenticating
|
||||
| Status::Reauthenticated
|
||||
| Status::ReconnectionError { .. } => false,
|
||||
Status::Connected { .. } | Status::Connecting | Status::Reconnecting => {
|
||||
return ConnectionResult::Result(Ok(()));
|
||||
@@ -1670,21 +1696,10 @@ impl Client {
|
||||
);
|
||||
cx.spawn(async move |_| match future.await {
|
||||
Ok(()) => {
|
||||
log::debug!(
|
||||
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
original_sender_id,
|
||||
type_name
|
||||
);
|
||||
log::debug!("rpc message handled. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}");
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
|
||||
client_id,
|
||||
original_sender_id,
|
||||
type_name,
|
||||
error
|
||||
);
|
||||
log::error!("error handling message. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}, error:{error:#}");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -216,7 +216,9 @@ impl UserStore {
|
||||
return Ok(());
|
||||
};
|
||||
match status {
|
||||
Status::Authenticated | Status::Connected { .. } => {
|
||||
Status::Authenticated
|
||||
| Status::Reauthenticated
|
||||
| Status::Connected { .. } => {
|
||||
if let Some(user_id) = client.user_id() {
|
||||
let response = client
|
||||
.cloud_client()
|
||||
|
||||
@@ -175,6 +175,7 @@ CREATE TABLE "language_servers" (
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"name" VARCHAR NOT NULL,
|
||||
"capabilities" TEXT NOT NULL,
|
||||
"worktree_id" BIGINT,
|
||||
PRIMARY KEY (project_id, id)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE language_servers
|
||||
ADD COLUMN worktree_id BIGINT;
|
||||
@@ -694,6 +694,7 @@ impl Database {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
id: ActiveValue::set(server.id as i64),
|
||||
name: ActiveValue::set(server.name.clone()),
|
||||
worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)),
|
||||
capabilities: ActiveValue::set(update.capabilities.clone()),
|
||||
})
|
||||
.on_conflict(
|
||||
@@ -704,6 +705,7 @@ impl Database {
|
||||
.update_columns([
|
||||
language_server::Column::Name,
|
||||
language_server::Column::Capabilities,
|
||||
language_server::Column::WorktreeId,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -1065,7 +1067,7 @@ impl Database {
|
||||
server: proto::LanguageServer {
|
||||
id: language_server.id as u64,
|
||||
name: language_server.name,
|
||||
worktree_id: None,
|
||||
worktree_id: language_server.worktree_id.map(|id| id as u64),
|
||||
},
|
||||
capabilities: language_server.capabilities,
|
||||
})
|
||||
|
||||
@@ -809,7 +809,7 @@ impl Database {
|
||||
server: proto::LanguageServer {
|
||||
id: language_server.id as u64,
|
||||
name: language_server.name,
|
||||
worktree_id: None,
|
||||
worktree_id: language_server.worktree_id.map(|id| id as u64),
|
||||
},
|
||||
capabilities: language_server.capabilities,
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct Model {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub capabilities: String,
|
||||
pub worktree_id: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -476,7 +476,9 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context);
|
||||
.add_message_handler(update_context)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>);
|
||||
|
||||
Arc::new(server)
|
||||
}
|
||||
|
||||
@@ -369,7 +369,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
@@ -488,7 +488,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
@@ -615,7 +615,7 @@ async fn test_collaborating_with_code_actions(
|
||||
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(params.range.start, lsp::Position::new(0, 0));
|
||||
assert_eq!(params.range.end, lsp::Position::new(0, 0));
|
||||
@@ -637,7 +637,7 @@ async fn test_collaborating_with_code_actions(
|
||||
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(params.range.start, lsp::Position::new(1, 31));
|
||||
assert_eq!(params.range.end, lsp::Position::new(1, 31));
|
||||
@@ -649,7 +649,7 @@ async fn test_collaborating_with_code_actions(
|
||||
changes: Some(
|
||||
[
|
||||
(
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(1, 22),
|
||||
@@ -659,7 +659,7 @@ async fn test_collaborating_with_code_actions(
|
||||
)],
|
||||
),
|
||||
(
|
||||
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(0, 0),
|
||||
@@ -721,7 +721,7 @@ async fn test_collaborating_with_code_actions(
|
||||
changes: Some(
|
||||
[
|
||||
(
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(1, 22),
|
||||
@@ -731,7 +731,7 @@ async fn test_collaborating_with_code_actions(
|
||||
)],
|
||||
),
|
||||
(
|
||||
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(0, 0),
|
||||
@@ -949,14 +949,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
changes: Some(
|
||||
[
|
||||
(
|
||||
lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(),
|
||||
vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
"THREE".to_string(),
|
||||
)],
|
||||
),
|
||||
(
|
||||
lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(),
|
||||
vec![
|
||||
lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
@@ -1574,7 +1574,7 @@ async fn test_on_input_format_from_host_to_guest(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
@@ -1717,7 +1717,7 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document_position.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.text_document_position.position,
|
||||
@@ -1901,7 +1901,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
|
||||
Ok(Some(vec![lsp::InlayHint {
|
||||
@@ -2151,7 +2151,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
|
||||
let character = if other_hints { 0 } else { 2 };
|
||||
@@ -2332,7 +2332,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
requests_made.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(vec![lsp::ColorInformation {
|
||||
@@ -2621,11 +2621,11 @@ async fn test_lsp_pull_diagnostics(
|
||||
let requests_made = closure_diagnostics_pulls_made.clone();
|
||||
let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
|
||||
async move {
|
||||
let message = if lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
|
||||
let message = if lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
|
||||
== params.text_document.uri
|
||||
{
|
||||
expected_pull_diagnostic_main_message.to_string()
|
||||
} else if lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap()
|
||||
} else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
|
||||
== params.text_document.uri
|
||||
{
|
||||
expected_pull_diagnostic_lib_message.to_string()
|
||||
@@ -2717,7 +2717,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
items: vec![
|
||||
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
||||
lsp::WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
version: None,
|
||||
full_document_diagnostic_report:
|
||||
lsp::FullDocumentDiagnosticReport {
|
||||
@@ -2746,7 +2746,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
),
|
||||
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
||||
lsp::WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
version: None,
|
||||
full_document_diagnostic_report:
|
||||
lsp::FullDocumentDiagnosticReport {
|
||||
@@ -2821,7 +2821,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
@@ -2842,7 +2842,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
);
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
@@ -2870,7 +2870,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
items: vec![
|
||||
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
||||
lsp::WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
version: None,
|
||||
full_document_diagnostic_report:
|
||||
lsp::FullDocumentDiagnosticReport {
|
||||
@@ -2902,7 +2902,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
),
|
||||
lsp::WorkspaceDocumentDiagnosticReport::Full(
|
||||
lsp::WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
version: None,
|
||||
full_document_diagnostic_report:
|
||||
lsp::FullDocumentDiagnosticReport {
|
||||
@@ -3051,7 +3051,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
|
||||
items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
|
||||
lsp::WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
|
||||
version: None,
|
||||
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
|
||||
result_id: Some(format!(
|
||||
@@ -3425,16 +3425,16 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
Some(blame_entry("1b1b1b", 0..1)),
|
||||
Some(blame_entry("0d0d0d", 1..2)),
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
|
||||
Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
|
||||
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
|
||||
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
|
||||
]
|
||||
);
|
||||
|
||||
blame.update(cx, |blame, _| {
|
||||
for (idx, entry) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(entry).unwrap();
|
||||
for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(*buffer, entry).unwrap();
|
||||
assert_eq!(details.message, format!("message for idx-{}", idx));
|
||||
assert_eq!(
|
||||
details.permalink.unwrap().to_string(),
|
||||
@@ -3474,9 +3474,9 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
entries,
|
||||
vec![
|
||||
None,
|
||||
Some(blame_entry("0d0d0d", 1..2)),
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
|
||||
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
|
||||
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -3511,8 +3511,8 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
vec![
|
||||
None,
|
||||
None,
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
|
||||
Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -4040,7 +4040,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(params.position, lsp::Position::new(0, 0));
|
||||
Ok(Some(ExpandedMacro {
|
||||
@@ -4075,7 +4075,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
params.position,
|
||||
|
||||
@@ -4075,7 +4075,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
.await;
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
||||
@@ -4095,7 +4095,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
.unwrap();
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
||||
@@ -4169,7 +4169,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Simulate a language server reporting more errors for a file.
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![
|
||||
lsp::Diagnostic {
|
||||
@@ -4265,7 +4265,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Simulate a language server reporting no errors for a file.
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
|
||||
version: None,
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
@@ -4372,7 +4372,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
|
||||
for file_name in file_names {
|
||||
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
|
||||
&lsp::PublishDiagnosticsParams {
|
||||
uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
|
||||
version: None,
|
||||
diagnostics: vec![lsp::Diagnostic {
|
||||
severity: Some(lsp::DiagnosticSeverity::WARNING),
|
||||
@@ -4838,7 +4838,7 @@ async fn test_definition(
|
||||
|_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(
|
||||
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
),
|
||||
)))
|
||||
@@ -4876,7 +4876,7 @@ async fn test_definition(
|
||||
|_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(
|
||||
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
|
||||
),
|
||||
)))
|
||||
@@ -4914,7 +4914,7 @@ async fn test_definition(
|
||||
);
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(
|
||||
lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
|
||||
),
|
||||
)))
|
||||
@@ -5049,15 +5049,15 @@ async fn test_references(
|
||||
lsp_response_tx
|
||||
.unbounded_send(Ok(Some(vec![
|
||||
lsp::Location {
|
||||
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
|
||||
},
|
||||
lsp::Location {
|
||||
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
|
||||
},
|
||||
lsp::Location {
|
||||
uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
|
||||
},
|
||||
])))
|
||||
@@ -5625,7 +5625,7 @@ async fn test_project_symbols(
|
||||
lsp::SymbolInformation {
|
||||
name: "TWO".into(),
|
||||
location: lsp::Location {
|
||||
uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
|
||||
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
},
|
||||
kind: lsp::SymbolKind::CONSTANT,
|
||||
@@ -5737,7 +5737,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
|_, _| async move {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
|
||||
lsp::Location::new(
|
||||
lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(),
|
||||
lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
|
||||
),
|
||||
)))
|
||||
|
||||
@@ -1101,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
files
|
||||
.into_iter()
|
||||
.map(|file| lsp::Location {
|
||||
uri: lsp::Url::from_file_path(file).unwrap(),
|
||||
uri: lsp::Uri::from_file_path(file).unwrap(),
|
||||
range: Default::default(),
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -12,7 +12,9 @@ use language::{
|
||||
Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, ToOffset,
|
||||
language_settings::SoftWrap,
|
||||
};
|
||||
use project::{Completion, CompletionResponse, CompletionSource, search::SearchQuery};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionResponse, CompletionSource, search::SearchQuery,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
ops::Range,
|
||||
@@ -275,6 +277,7 @@ impl MessageEditor {
|
||||
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: Vec::new(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
}
|
||||
@@ -317,6 +320,7 @@ impl MessageEditor {
|
||||
|
||||
CompletionResponse {
|
||||
is_incomplete: completions.len() >= LIMIT,
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
completions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3047,7 +3047,7 @@ impl Render for CollabPanel {
|
||||
.on_action(cx.listener(CollabPanel::move_channel_down))
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.child(if !self.client.status().borrow().is_connected() {
|
||||
.child(if !self.client.status().borrow().is_or_was_connected() {
|
||||
self.render_signed_out(cx)
|
||||
} else {
|
||||
self.render_signed_in(window, cx)
|
||||
|
||||
@@ -66,5 +66,7 @@ fn notification_window_options(
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: None,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
tabbing_identifier: None,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -27,7 +27,7 @@ pub struct ChatPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
@@ -50,7 +50,7 @@ pub struct NotificationPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct PanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
@@ -66,7 +66,7 @@ pub struct PanelSettingsContent {
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct MessageEditorSettings {
|
||||
/// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
/// For example: typing `:wave:` gets replaced with `👋`.
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
};
|
||||
|
||||
const JSON_RPC_VERSION: &str = "2.0";
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
// Standard JSON-RPC error codes
|
||||
pub const PARSE_ERROR: i32 = -32700;
|
||||
@@ -60,6 +60,7 @@ pub(crate) struct Client {
|
||||
executor: BackgroundExecutor,
|
||||
#[allow(dead_code)]
|
||||
transport: Arc<dyn Transport>,
|
||||
request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -143,6 +144,7 @@ pub struct ModelContextServerBinary {
|
||||
pub executable: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -169,8 +171,9 @@ impl Client {
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
let timeout = binary.timeout.map(Duration::from_millis);
|
||||
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
|
||||
Self::new(server_id, server_name.into(), transport, cx)
|
||||
Self::new(server_id, server_name.into(), transport, timeout, cx)
|
||||
}
|
||||
|
||||
/// Creates a new Client instance for a context server.
|
||||
@@ -178,6 +181,7 @@ impl Client {
|
||||
server_id: ContextServerId,
|
||||
server_name: Arc<str>,
|
||||
transport: Arc<dyn Transport>,
|
||||
request_timeout: Option<Duration>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
|
||||
@@ -237,6 +241,7 @@ impl Client {
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
transport,
|
||||
request_timeout,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -327,8 +332,13 @@ impl Client {
|
||||
method: &str,
|
||||
params: impl Serialize,
|
||||
) -> Result<T> {
|
||||
self.request_with(method, params, None, Some(REQUEST_TIMEOUT))
|
||||
.await
|
||||
self.request_with(
|
||||
method,
|
||||
params,
|
||||
None,
|
||||
self.request_timeout.or(Some(DEFAULT_REQUEST_TIMEOUT)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn request_with<T: DeserializeOwned>(
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct ContextServerCommand {
|
||||
pub path: PathBuf,
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified.
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ContextServerCommand {
|
||||
@@ -123,6 +125,7 @@ impl ContextServer {
|
||||
executable: Path::new(&command.path).to_path_buf(),
|
||||
args: command.args.clone(),
|
||||
env: command.env.clone(),
|
||||
timeout: command.timeout,
|
||||
},
|
||||
working_directory,
|
||||
cx.clone(),
|
||||
@@ -131,6 +134,7 @@ impl ContextServer {
|
||||
client::ContextServerId(self.id.0.clone()),
|
||||
self.id().0,
|
||||
transport.clone(),
|
||||
None,
|
||||
cx.clone(),
|
||||
)?,
|
||||
})
|
||||
|
||||
@@ -197,7 +197,7 @@ impl Status {
|
||||
}
|
||||
|
||||
struct RegisteredBuffer {
|
||||
uri: lsp::Url,
|
||||
uri: lsp::Uri,
|
||||
language_id: String,
|
||||
snapshot: BufferSnapshot,
|
||||
snapshot_version: i32,
|
||||
@@ -1108,9 +1108,9 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
|
||||
.unwrap_or_else(|| "plaintext".to_string())
|
||||
}
|
||||
|
||||
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
|
||||
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Uri, ()> {
|
||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
||||
lsp::Url::from_file_path(file.abs_path(cx))
|
||||
lsp::Uri::from_file_path(file.abs_path(cx))
|
||||
} else {
|
||||
format!("buffer://{}", buffer.entity_id())
|
||||
.parse()
|
||||
@@ -1201,7 +1201,7 @@ mod tests {
|
||||
let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
|
||||
let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
|
||||
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
|
||||
let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
@@ -1219,7 +1219,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
|
||||
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
|
||||
let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
@@ -1270,7 +1270,7 @@ mod tests {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
}
|
||||
);
|
||||
let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap();
|
||||
let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap();
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
|
||||
@@ -62,12 +62,6 @@ impl CopilotChatConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
// Copilot's base model; defined by Microsoft in premium requests table
|
||||
// This will be moved to the front of the Copilot model list, and will be used for
|
||||
// 'fast' requests (e.g. title generation)
|
||||
// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
|
||||
const DEFAULT_MODEL_ID: &str = "gpt-4.1";
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
@@ -101,22 +95,39 @@ where
|
||||
Ok(models)
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct Model {
|
||||
billing: ModelBilling,
|
||||
capabilities: ModelCapabilities,
|
||||
id: String,
|
||||
name: String,
|
||||
policy: Option<ModelPolicy>,
|
||||
vendor: ModelVendor,
|
||||
is_chat_default: bool,
|
||||
// The model with this value true is selected by VSCode copilot if a premium request limit is
|
||||
// reached. Zed does not currently implement this behaviour
|
||||
is_chat_fallback: bool,
|
||||
model_picker_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
struct ModelBilling {
|
||||
is_premium: bool,
|
||||
multiplier: f64,
|
||||
// List of plans a model is restricted to
|
||||
// Field is not present if a model is available for all plans
|
||||
#[serde(default)]
|
||||
restricted_to: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ModelCapabilities {
|
||||
family: String,
|
||||
#[serde(default)]
|
||||
limits: ModelLimits,
|
||||
supports: ModelSupportedFeatures,
|
||||
#[serde(rename = "type")]
|
||||
model_type: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
@@ -153,6 +164,8 @@ pub enum ModelVendor {
|
||||
OpenAI,
|
||||
Google,
|
||||
Anthropic,
|
||||
#[serde(rename = "xAI")]
|
||||
XAI,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
|
||||
@@ -602,6 +615,7 @@ async fn get_models(
|
||||
.into_iter()
|
||||
.filter(|model| {
|
||||
model.model_picker_enabled
|
||||
&& model.capabilities.model_type.as_str() == "chat"
|
||||
&& model
|
||||
.policy
|
||||
.as_ref()
|
||||
@@ -610,9 +624,7 @@ async fn get_models(
|
||||
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
|
||||
.collect();
|
||||
|
||||
if let Some(default_model_position) =
|
||||
models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
|
||||
{
|
||||
if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) {
|
||||
let default_model = models.remove(default_model_position);
|
||||
models.insert(0, default_model);
|
||||
}
|
||||
@@ -630,7 +642,9 @@ async fn request_models(
|
||||
.uri(models_url.as_ref())
|
||||
.header("Authorization", format!("Bearer {}", api_token))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
.header("Copilot-Integration-Id", "vscode-chat")
|
||||
.header("Editor-Version", "vscode/1.103.2")
|
||||
.header("x-github-api-version", "2025-05-01");
|
||||
|
||||
let request = request_builder.body(AsyncBody::empty())?;
|
||||
|
||||
@@ -801,6 +815,10 @@ mod tests {
|
||||
let json = r#"{
|
||||
"data": [
|
||||
{
|
||||
"billing": {
|
||||
"is_premium": false,
|
||||
"multiplier": 0
|
||||
},
|
||||
"capabilities": {
|
||||
"family": "gpt-4",
|
||||
"limits": {
|
||||
@@ -814,6 +832,8 @@ mod tests {
|
||||
"type": "chat"
|
||||
},
|
||||
"id": "gpt-4",
|
||||
"is_chat_default": false,
|
||||
"is_chat_fallback": false,
|
||||
"model_picker_enabled": false,
|
||||
"name": "GPT 4",
|
||||
"object": "model",
|
||||
@@ -825,6 +845,16 @@ mod tests {
|
||||
"some-unknown-field": 123
|
||||
},
|
||||
{
|
||||
"billing": {
|
||||
"is_premium": true,
|
||||
"multiplier": 1,
|
||||
"restricted_to": [
|
||||
"pro",
|
||||
"pro_plus",
|
||||
"business",
|
||||
"enterprise"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"family": "claude-3.7-sonnet",
|
||||
"limits": {
|
||||
@@ -848,6 +878,8 @@ mod tests {
|
||||
"type": "chat"
|
||||
},
|
||||
"id": "claude-3.7-sonnet",
|
||||
"is_chat_default": false,
|
||||
"is_chat_fallback": false,
|
||||
"model_picker_enabled": true,
|
||||
"name": "Claude 3.7 Sonnet",
|
||||
"object": "model",
|
||||
|
||||
@@ -102,7 +102,7 @@ pub struct GetCompletionsDocument {
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp::Url,
|
||||
pub uri: lsp::Uri,
|
||||
pub relative_path: String,
|
||||
pub position: lsp::Position,
|
||||
pub version: usize,
|
||||
|
||||
@@ -2,9 +2,9 @@ use dap_types::SteppingGranularity;
|
||||
use gpui::{App, Global};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DebugPanelDockPosition {
|
||||
Left,
|
||||
@@ -12,12 +12,16 @@ pub enum DebugPanelDockPosition {
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
|
||||
#[serde(default)]
|
||||
// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
|
||||
// it means the defaults will override previously set values if a single key is missing
|
||||
#[settings_ui(group = "Debugger", path = "debugger")]
|
||||
pub struct DebuggerSettings {
|
||||
/// Determines the stepping granularity.
|
||||
///
|
||||
/// Default: line
|
||||
#[settings_ui(skip)]
|
||||
pub stepping_granularity: SteppingGranularity,
|
||||
/// Whether the breakpoints should be reused across Zed sessions.
|
||||
///
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user