Merge branch 'main' of https://github.com/jacobtread/zed into fix-file-icons-multiple-extensions

This commit is contained in:
Jacobtread 2025-08-18 00:00:42 +12:00
commit 73322a54f2
490 changed files with 21892 additions and 14140 deletions

View file

@ -25,6 +25,8 @@ third-party = [
{ name = "reqwest", version = "0.11.27" }, { name = "reqwest", version = "0.11.27" },
# build of remote_server should not include scap / its x11 dependency # build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
# build of remote_server should not need to include on libalsa through rodio
{ name = "rodio" },
] ]
[final-excludes] [final-excludes]
@ -32,7 +34,6 @@ workspace-members = [
"zed_extension_api", "zed_extension_api",
# exclude all extensions # exclude all extensions
"zed_emmet",
"zed_glsl", "zed_glsl",
"zed_html", "zed_html",
"zed_proto", "zed_proto",

View file

@ -0,0 +1,35 @@
name: Bug Report (Windows Alpha)
description: Zed Windows Alpha Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows Alpha: <a short description of the Windows bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one-line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true

View file

@ -20,7 +20,168 @@ runs:
with: with:
node-version: "18" 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 - name: Run tests
shell: powershell shell: powershell
working-directory: ${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }}
run: cargo nextest run --workspace --no-fail-fast 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
continue-on-error: true
- 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
}

View file

@ -718,7 +718,7 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
runs-on: github-8vcpu-ubuntu-2404 runs-on: github-8vcpu-ubuntu-2404
if: | if: |
( startsWith(github.ref, 'refs/tags/v') false && ( startsWith(github.ref, 'refs/tags/v')
|| contains(github.event.pull_request.labels.*.name, 'run-bundling') ) || contains(github.event.pull_request.labels.*.name, 'run-bundling') )
needs: [linux_tests] needs: [linux_tests]
name: Build Zed on FreeBSD name: Build Zed on FreeBSD
@ -851,3 +851,12 @@ jobs:
run: gh release edit "$GITHUB_REF_NAME" --draft=false run: gh release edit "$GITHUB_REF_NAME" --draft=false
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Sentry release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
environment: production

View file

@ -316,3 +316,12 @@ jobs:
git config user.email github-actions@github.com git config user.email github-actions@github.com
git tag -f nightly git tag -f nightly
git push origin nightly --force git push origin nightly --force
- name: Create Sentry release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c # v3
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
with:
environment: production

View file

@ -3,7 +3,7 @@ name: Run Unit Evals
on: on:
schedule: schedule:
# GitHub might drop jobs at busy times, so we choose a random time in the middle of the night. # GitHub might drop jobs at busy times, so we choose a random time in the middle of the night.
- cron: "47 1 * * *" - cron: "47 1 * * 2"
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:

328
Cargo.lock generated
View file

@ -6,32 +6,65 @@ version = 4
name = "acp_thread" name = "acp_thread"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"action_log",
"agent",
"agent-client-protocol", "agent-client-protocol",
"anyhow", "anyhow",
"assistant_tool",
"buffer_diff", "buffer_diff",
"collections",
"editor", "editor",
"env_logger 0.11.8", "env_logger 0.11.8",
"file_icons",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
"language_model",
"markdown", "markdown",
"parking_lot", "parking_lot",
"project", "project",
"prompt_store",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde_json", "serde_json",
"settings", "settings",
"smol", "smol",
"tempfile", "tempfile",
"terminal",
"ui", "ui",
"url",
"util", "util",
"uuid",
"watch",
"workspace-hack", "workspace-hack",
] ]
[[package]]
name = "action_log"
version = "0.1.0"
dependencies = [
"anyhow",
"buffer_diff",
"clock",
"collections",
"ctor",
"futures 0.3.31",
"gpui",
"indoc",
"language",
"log",
"pretty_assertions",
"project",
"rand 0.8.5",
"serde_json",
"settings",
"text",
"util",
"watch",
"workspace-hack",
"zlog",
]
[[package]] [[package]]
name = "activity_indicator" name = "activity_indicator"
version = "0.1.0" version = "0.1.0"
@ -84,6 +117,7 @@ dependencies = [
name = "agent" name = "agent"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"action_log",
"agent_settings", "agent_settings",
"anyhow", "anyhow",
"assistant_context", "assistant_context",
@ -138,9 +172,9 @@ dependencies = [
[[package]] [[package]]
name = "agent-client-protocol" name = "agent-client-protocol"
version = "0.0.23" version = "0.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fad72b7b8ee4331b3a4c8d43c107e982a4725564b4ee658ae5c4e79d2b486e8" checksum = "2ab66add8be8d6a963f5bf4070045c1bbf36472837654c73e2298dd16bda5bf7"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures 0.3.31", "futures 0.3.31",
@ -156,23 +190,29 @@ name = "agent2"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"acp_thread", "acp_thread",
"action_log",
"agent-client-protocol", "agent-client-protocol",
"agent_servers", "agent_servers",
"agent_settings", "agent_settings",
"anyhow", "anyhow",
"assistant_tool", "assistant_tool",
"assistant_tools", "assistant_tools",
"chrono",
"client", "client",
"clock", "clock",
"cloud_llm_client", "cloud_llm_client",
"collections", "collections",
"context_server",
"ctor", "ctor",
"editor",
"env_logger 0.11.8", "env_logger 0.11.8",
"fs", "fs",
"futures 0.3.31", "futures 0.3.31",
"gpui", "gpui",
"gpui_tokio", "gpui_tokio",
"handlebars 4.5.0", "handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc", "indoc",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
@ -180,7 +220,9 @@ dependencies = [
"language_models", "language_models",
"log", "log",
"lsp", "lsp",
"open",
"paths", "paths",
"portable-pty",
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store", "prompt_store",
@ -191,12 +233,22 @@ dependencies = [
"serde_json", "serde_json",
"settings", "settings",
"smol", "smol",
"task",
"tempfile",
"terminal",
"text",
"theme",
"tree-sitter-rust",
"ui", "ui",
"unindent",
"util", "util",
"uuid", "uuid",
"watch", "watch",
"web_search",
"which 6.0.3",
"workspace-hack", "workspace-hack",
"worktree", "worktree",
"zlog",
] ]
[[package]] [[package]]
@ -261,6 +313,7 @@ name = "agent_ui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"acp_thread", "acp_thread",
"action_log",
"agent", "agent",
"agent-client-protocol", "agent-client-protocol",
"agent2", "agent2",
@ -294,7 +347,6 @@ dependencies = [
"gpui", "gpui",
"html_to_markdown", "html_to_markdown",
"http_client", "http_client",
"indexed_docs",
"indoc", "indoc",
"inventory", "inventory",
"itertools 0.14.0", "itertools 0.14.0",
@ -342,6 +394,7 @@ dependencies = [
"ui", "ui",
"ui_input", "ui_input",
"unindent", "unindent",
"url",
"urlencoding", "urlencoding",
"util", "util",
"uuid", "uuid",
@ -818,7 +871,6 @@ dependencies = [
"gpui", "gpui",
"html_to_markdown", "html_to_markdown",
"http_client", "http_client",
"indexed_docs",
"language", "language",
"pretty_assertions", "pretty_assertions",
"project", "project",
@ -842,13 +894,13 @@ dependencies = [
name = "assistant_tool" name = "assistant_tool"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"action_log",
"anyhow", "anyhow",
"buffer_diff", "buffer_diff",
"clock", "clock",
"collections", "collections",
"ctor", "ctor",
"derive_more 0.99.19", "derive_more 0.99.19",
"futures 0.3.31",
"gpui", "gpui",
"icons", "icons",
"indoc", "indoc",
@ -865,7 +917,6 @@ dependencies = [
"settings", "settings",
"text", "text",
"util", "util",
"watch",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zlog", "zlog",
@ -875,6 +926,7 @@ dependencies = [
name = "assistant_tools" name = "assistant_tools"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"action_log",
"agent_settings", "agent_settings",
"anyhow", "anyhow",
"assistant_tool", "assistant_tool",
@ -1208,26 +1260,6 @@ dependencies = [
"syn 2.0.101", "syn 2.0.101",
] ]
[[package]]
name = "async-stripe"
version = "0.40.0"
source = "git+https://github.com/zed-industries/async-stripe?rev=3672dd4efb7181aa597bf580bf5a2f5d23db6735#3672dd4efb7181aa597bf580bf5a2f5d23db6735"
dependencies = [
"chrono",
"futures-util",
"http-types",
"hyper 0.14.32",
"hyper-rustls 0.24.2",
"serde",
"serde_json",
"serde_path_to_error",
"serde_qs 0.10.1",
"smart-default 0.6.0",
"smol_str 0.1.24",
"thiserror 1.0.69",
"tokio",
]
[[package]] [[package]]
name = "async-tar" name = "async-tar"
version = "0.5.0" version = "0.5.0"
@ -1250,9 +1282,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.88" version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2029,12 +2061,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@ -3227,7 +3253,6 @@ dependencies = [
"anyhow", "anyhow",
"assistant_context", "assistant_context",
"assistant_slash_command", "assistant_slash_command",
"async-stripe",
"async-trait", "async-trait",
"async-tungstenite", "async-tungstenite",
"audio", "audio",
@ -3243,7 +3268,6 @@ dependencies = [
"chrono", "chrono",
"client", "client",
"clock", "clock",
"cloud_llm_client",
"collab_ui", "collab_ui",
"collections", "collections",
"command_palette_hooks", "command_palette_hooks",
@ -3254,7 +3278,6 @@ dependencies = [
"dap_adapters", "dap_adapters",
"dashmap 6.1.0", "dashmap 6.1.0",
"debugger_ui", "debugger_ui",
"derive_more 0.99.19",
"editor", "editor",
"envy", "envy",
"extension", "extension",
@ -3270,7 +3293,6 @@ dependencies = [
"http_client", "http_client",
"hyper 0.14.32", "hyper 0.14.32",
"indoc", "indoc",
"jsonwebtoken",
"language", "language",
"language_model", "language_model",
"livekit_api", "livekit_api",
@ -3316,7 +3338,6 @@ dependencies = [
"telemetry_events", "telemetry_events",
"text", "text",
"theme", "theme",
"thiserror 2.0.12",
"time", "time",
"tokio", "tokio",
"toml 0.8.20", "toml 0.8.20",
@ -3818,7 +3839,7 @@ dependencies = [
"rustc-hash 1.1.0", "rustc-hash 1.1.0",
"rustybuzz 0.14.1", "rustybuzz 0.14.1",
"self_cell", "self_cell",
"smol_str 0.2.2", "smol_str",
"swash", "swash",
"sys-locale", "sys-locale",
"ttf-parser 0.21.1", "ttf-parser 0.21.1",
@ -4014,6 +4035,9 @@ dependencies = [
"log", "log",
"minidumper", "minidumper",
"paths", "paths",
"release_channel",
"serde",
"serde_json",
"smol", "smol",
"workspace-hack", "workspace-hack",
] ]
@ -6321,17 +6345,6 @@ dependencies = [
"windows-targets 0.48.5", "windows-targets 0.48.5",
] ]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.15"
@ -6396,6 +6409,7 @@ dependencies = [
"log", "log",
"parking_lot", "parking_lot",
"pretty_assertions", "pretty_assertions",
"rand 0.8.5",
"regex", "regex",
"rope", "rope",
"schemars", "schemars",
@ -7827,6 +7841,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.27.0" version = "0.27.0"
@ -7928,27 +7948,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "http-types"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad"
dependencies = [
"anyhow",
"async-channel 1.9.0",
"base64 0.13.1",
"futures-lite 1.13.0",
"http 0.2.12",
"infer",
"pin-project-lite",
"rand 0.7.3",
"serde",
"serde_json",
"serde_qs 0.8.5",
"serde_urlencoded",
"url",
]
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
@ -8382,34 +8381,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexed_docs"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"cargo_metadata",
"collections",
"derive_more 0.99.19",
"extension",
"fs",
"futures 0.3.31",
"fuzzy",
"gpui",
"heed",
"html_to_markdown",
"http_client",
"indexmap",
"indoc",
"parking_lot",
"paths",
"pretty_assertions",
"serde",
"strum 0.27.1",
"util",
"workspace-hack",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.9.0" version = "2.9.0"
@ -8427,12 +8398,6 @@ version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "infer"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
[[package]] [[package]]
name = "inherent" name = "inherent"
version = "1.0.12" version = "1.0.12"
@ -9655,6 +9620,7 @@ dependencies = [
"objc", "objc",
"parking_lot", "parking_lot",
"postage", "postage",
"rodio",
"scap", "scap",
"serde", "serde",
"serde_json", "serde_json",
@ -10208,7 +10174,7 @@ dependencies = [
"num-traits", "num-traits",
"range-map", "range-map",
"scroll", "scroll",
"smart-default 0.7.1", "smart-default",
] ]
[[package]] [[package]]
@ -11100,14 +11066,13 @@ dependencies = [
"ai_onboarding", "ai_onboarding",
"anyhow", "anyhow",
"client", "client",
"command_palette_hooks",
"component", "component",
"db", "db",
"documented", "documented",
"editor", "editor",
"feature_flags",
"fs", "fs",
"fuzzy", "fuzzy",
"git",
"gpui", "gpui",
"itertools 0.14.0", "itertools 0.14.0",
"language", "language",
@ -11119,6 +11084,7 @@ dependencies = [
"schemars", "schemars",
"serde", "serde",
"settings", "settings",
"telemetry",
"theme", "theme",
"ui", "ui",
"util", "util",
@ -11194,6 +11160,7 @@ dependencies = [
"anyhow", "anyhow",
"futures 0.3.31", "futures 0.3.31",
"http_client", "http_client",
"log",
"schemars", "schemars",
"serde", "serde",
"serde_json", "serde_json",
@ -13081,19 +13048,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -13115,16 +13069,6 @@ dependencies = [
"rand_core 0.9.3", "rand_core 0.9.3",
] ]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.3.1" version = "0.3.1"
@ -13145,15 +13089,6 @@ dependencies = [
"rand_core 0.9.3", "rand_core 0.9.3",
] ]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.6.4" version = "0.6.4"
@ -13172,15 +13107,6 @@ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.2",
] ]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]] [[package]]
name = "range-map" name = "range-map"
version = "0.2.0" version = "0.2.0"
@ -13522,6 +13448,7 @@ dependencies = [
name = "remote_server" name = "remote_server"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"action_log",
"anyhow", "anyhow",
"askpass", "askpass",
"assistant_tool", "assistant_tool",
@ -13914,6 +13841,7 @@ checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
dependencies = [ dependencies = [
"cpal", "cpal",
"dasp_sample", "dasp_sample",
"hound",
"num-rational", "num-rational",
"symphonia", "symphonia",
"tracing", "tracing",
@ -14833,28 +14761,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_qs"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6"
dependencies = [
"percent-encoding",
"serde",
"thiserror 1.0.69",
]
[[package]]
name = "serde_qs"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa"
dependencies = [
"percent-encoding",
"serde",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "serde_repr" name = "serde_repr"
version = "0.1.20" version = "0.1.20"
@ -14996,8 +14902,10 @@ dependencies = [
"ui", "ui",
"ui_input", "ui_input",
"util", "util",
"vim",
"workspace", "workspace",
"workspace-hack", "workspace-hack",
"zed_actions",
] ]
[[package]] [[package]]
@ -15229,17 +15137,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "smart-default"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "smart-default" name = "smart-default"
version = "0.7.1" version = "0.7.1"
@ -15268,15 +15165,6 @@ dependencies = [
"futures-lite 2.6.0", "futures-lite 2.6.0",
] ]
[[package]]
name = "smol_str"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "smol_str" name = "smol_str"
version = "0.2.2" version = "0.2.2"
@ -17978,6 +17866,7 @@ dependencies = [
"command_palette_hooks", "command_palette_hooks",
"db", "db",
"editor", "editor",
"env_logger 0.11.8",
"futures 0.3.31", "futures 0.3.31",
"git_ui", "git_ui",
"gpui", "gpui",
@ -18124,12 +18013,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -18831,33 +18714,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "welcome"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"component",
"db",
"documented",
"editor",
"fuzzy",
"gpui",
"install_cli",
"language",
"picker",
"project",
"serde",
"settings",
"telemetry",
"ui",
"util",
"vim_mode_setting",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]] [[package]]
name = "which" name = "which"
version = "4.4.2" version = "4.4.2"
@ -20243,7 +20099,7 @@ dependencies = [
[[package]] [[package]]
name = "xim" name = "xim"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d"
dependencies = [ dependencies = [
"ahash 0.8.11", "ahash 0.8.11",
"hashbrown 0.14.5", "hashbrown 0.14.5",
@ -20256,7 +20112,7 @@ dependencies = [
[[package]] [[package]]
name = "xim-ctext" name = "xim-ctext"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d"
dependencies = [ dependencies = [
"encoding_rs", "encoding_rs",
] ]
@ -20264,7 +20120,7 @@ dependencies = [
[[package]] [[package]]
name = "xim-parser" name = "xim-parser"
version = "0.2.1" version = "0.2.1"
source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" source = "git+https://github.com/zed-industries/xim-rs?rev=c0a70c1bd2ce197364216e5e818a2cb3adb99a8d#c0a70c1bd2ce197364216e5e818a2cb3adb99a8d"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
] ]
@ -20472,7 +20328,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.200.0" version = "0.201.0"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"agent", "agent",
@ -20542,6 +20398,7 @@ dependencies = [
"language_tools", "language_tools",
"languages", "languages",
"libc", "libc",
"livekit_client",
"log", "log",
"markdown", "markdown",
"markdown_preview", "markdown_preview",
@ -20612,7 +20469,6 @@ dependencies = [
"watch", "watch",
"web_search", "web_search",
"web_search_providers", "web_search_providers",
"welcome",
"windows 0.61.1", "windows 0.61.1",
"winresource", "winresource",
"workspace", "workspace",
@ -20634,13 +20490,6 @@ dependencies = [
"workspace-hack", "workspace-hack",
] ]
[[package]]
name = "zed_emmet"
version = "0.0.4"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]] [[package]]
name = "zed_extension_api" name = "zed_extension_api"
version = "0.1.0" version = "0.1.0"
@ -20875,6 +20724,7 @@ dependencies = [
"menu", "menu",
"postage", "postage",
"project", "project",
"rand 0.8.5",
"regex", "regex",
"release_channel", "release_channel",
"reqwest_client", "reqwest_client",

View file

@ -2,6 +2,7 @@
resolver = "2" resolver = "2"
members = [ members = [
"crates/acp_thread", "crates/acp_thread",
"crates/action_log",
"crates/activity_indicator", "crates/activity_indicator",
"crates/agent", "crates/agent",
"crates/agent2", "crates/agent2",
@ -80,7 +81,6 @@ members = [
"crates/http_client_tls", "crates/http_client_tls",
"crates/icons", "crates/icons",
"crates/image_viewer", "crates/image_viewer",
"crates/indexed_docs",
"crates/edit_prediction", "crates/edit_prediction",
"crates/edit_prediction_button", "crates/edit_prediction_button",
"crates/inspector_ui", "crates/inspector_ui",
@ -184,7 +184,6 @@ members = [
"crates/watch", "crates/watch",
"crates/web_search", "crates/web_search",
"crates/web_search_providers", "crates/web_search_providers",
"crates/welcome",
"crates/workspace", "crates/workspace",
"crates/worktree", "crates/worktree",
"crates/x_ai", "crates/x_ai",
@ -199,7 +198,6 @@ members = [
# Extensions # Extensions
# #
"extensions/emmet",
"extensions/glsl", "extensions/glsl",
"extensions/html", "extensions/html",
"extensions/proto", "extensions/proto",
@ -229,6 +227,7 @@ edition = "2024"
# #
acp_thread = { path = "crates/acp_thread" } acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" } agent = { path = "crates/agent" }
agent2 = { path = "crates/agent2" } agent2 = { path = "crates/agent2" }
activity_indicator = { path = "crates/activity_indicator" } activity_indicator = { path = "crates/activity_indicator" }
@ -305,7 +304,6 @@ http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" } http_client_tls = { path = "crates/http_client_tls" }
icons = { path = "crates/icons" } icons = { path = "crates/icons" }
image_viewer = { path = "crates/image_viewer" } image_viewer = { path = "crates/image_viewer" }
indexed_docs = { path = "crates/indexed_docs" }
edit_prediction = { path = "crates/edit_prediction" } edit_prediction = { path = "crates/edit_prediction" }
edit_prediction_button = { path = "crates/edit_prediction_button" } edit_prediction_button = { path = "crates/edit_prediction_button" }
inspector_ui = { path = "crates/inspector_ui" } inspector_ui = { path = "crates/inspector_ui" }
@ -362,6 +360,7 @@ remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" } repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" } reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" } rich_text = { path = "crates/rich_text" }
rodio = { version = "0.21.1", default-features = false }
rope = { path = "crates/rope" } rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" } rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" } rules_library = { path = "crates/rules_library" }
@ -410,7 +409,6 @@ vim_mode_setting = { path = "crates/vim_mode_setting" }
watch = { path = "crates/watch" } watch = { path = "crates/watch" }
web_search = { path = "crates/web_search" } web_search = { path = "crates/web_search" }
web_search_providers = { path = "crates/web_search_providers" } web_search_providers = { path = "crates/web_search_providers" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" } workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" } worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" } x_ai = { path = "crates/x_ai" }
@ -425,7 +423,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# #
agentic-coding-protocol = "0.0.10" agentic-coding-protocol = "0.0.10"
agent-client-protocol = "0.0.23" agent-client-protocol = "0.0.25"
aho-corasick = "1.1" aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" } alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14" any_vec = "0.14"
@ -666,20 +664,6 @@ workspace-hack = "0.1.0"
yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" } yawc = { git = "https://github.com/deviant-forks/yawc", rev = "1899688f3e69ace4545aceb97b2a13881cf26142" }
zstd = "0.11" zstd = "0.11"
[workspace.dependencies.async-stripe]
git = "https://github.com/zed-industries/async-stripe"
rev = "3672dd4efb7181aa597bf580bf5a2f5d23db6735"
default-features = false
features = [
"runtime-tokio-hyper-rustls",
"billing",
"checkout",
"events",
# The features below are only enabled to get the `events` feature to build.
"chrono",
"connect",
]
[workspace.dependencies.windows] [workspace.dependencies.windows]
version = "0.61" version = "0.61"
features = [ features = [
@ -712,6 +696,7 @@ features = [
"Win32_System_LibraryLoader", "Win32_System_LibraryLoader",
"Win32_System_Memory", "Win32_System_Memory",
"Win32_System_Ole", "Win32_System_Ole",
"Win32_System_Performance",
"Win32_System_Pipes", "Win32_System_Pipes",
"Win32_System_SystemInformation", "Win32_System_SystemInformation",
"Win32_System_SystemServices", "Win32_System_SystemServices",
@ -839,6 +824,7 @@ style = { level = "allow", priority = -1 }
module_inception = { level = "deny" } module_inception = { level = "deny" }
question_mark = { level = "deny" } question_mark = { level = "deny" }
redundant_closure = { level = "deny" } redundant_closure = { level = "deny" }
declare_interior_mutable_const = { level = "deny" }
# Individual rules that have violations in the codebase: # Individual rules that have violations in the codebase:
type_complexity = "allow" type_complexity = "allow"
# We often return trait objects from `new` functions. # We often return trait objects from `new` functions.

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM rust:1.88-bookworm as builder FROM rust:1.89-bookworm as builder
WORKDIR app WORKDIR app
COPY . . COPY . .

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,8 +1,9 @@
Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" Copyright 2019 The Lilex Project Authors (https://github.com/mishamyrt/Lilex)
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL https://scripts.sil.org/OFL
----------------------------------------------------------- -----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
@ -89,4 +90,4 @@ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE. OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5.77778L4.25556 4.52222C5.26054 3.55068 6.6022 3.00526 8 3C9.32608 3 10.5979 3.52678 11.5355 4.46447C12.2339 5.16285 12.7044 6.04656 12.899 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12.8989 5.77778L11.6434 4.52222C10.6384 3.55068 9.29673 3.00526 7.89893 3C6.57285 3 5.30103 3.52678 4.36343 4.46447C3.78887 5.03901 3.36856 5.73897 3.12921 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3V5.77778H5.77778" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12.8989 3V5.77778H10.1211" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.0001 10.2222L11.7445 11.4778C10.7395 12.4493 9.39788 12.9947 8.00008 13C6.67399 13 5.40222 12.4732 4.46454 11.5355C3.76616 10.8372 3.29571 9.95344 3.10107 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3.1012 10.2222L4.3568 11.4778C5.3618 12.4493 6.70342 12.9947 8.10122 13C9.42731 13 10.6991 12.4732 11.6368 11.5355C12.2163 10.956 12.6389 10.2487 12.8772 9.47994" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.2224 10.2222H13.0002V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5.87891 10.2222H3.10111V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 854 B

Before After
Before After

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12.429 2H9.57A.571.571 0 0 0 9 2.571V5.43c0 .315.256.571.571.571h2.858A.571.571 0 0 0 13 5.429V2.57A.571.571 0 0 0 12.429 2ZM6.5 13V4.643A.643.643 0 0 0 5.857 4H2.643A.643.643 0 0 0 2 4.643v7.714a.643.643 0 0 0 .643.643h7.714a.643.643 0 0 0 .643-.643V9.143a.643.643 0 0 0-.643-.643H2"/></svg> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 13.5V5.143C7 4.97247 6.93226 4.80892 6.81167 4.68833C6.69108 4.56774 6.52753 4.5 6.357 4.5H3.143C2.97247 4.5 2.80892 4.56774 2.68833 4.68833C2.56774 4.80892 2.5 4.97247 2.5 5.143V12.857C2.5 12.9414 2.51663 13.0251 2.54895 13.1031C2.58126 13.1811 2.62862 13.252 2.68833 13.3117C2.74804 13.3714 2.81892 13.4187 2.89693 13.4511C2.97495 13.4834 3.05856 13.5 3.143 13.5H10.857C10.9414 13.5 11.0251 13.4834 11.1031 13.4511C11.1811 13.4187 11.252 13.3714 11.3117 13.3117C11.3714 13.252 11.4187 13.1811 11.4511 13.1031C11.4834 13.0251 11.5 12.9414 11.5 12.857V9.643C11.5 9.47247 11.4323 9.30892 11.3117 9.18833C11.1911 9.06774 11.0275 9 10.857 9H2.5M12.929 2.5H10.07C9.91873 2.50026 9.77376 2.56054 9.66689 2.6676C9.56002 2.77465 9.5 2.91973 9.5 3.071V5.93C9.5 6.245 9.756 6.501 10.071 6.501H12.929C13.0041 6.501 13.0784 6.4862 13.1477 6.45744C13.2171 6.42868 13.2801 6.38653 13.3331 6.3334C13.3861 6.28028 13.4282 6.21721 13.4568 6.14782C13.4855 6.07843 13.5001 6.00407 13.5 5.929V3.07C13.4997 2.91873 13.4395 2.77376 13.3324 2.66689C13.2254 2.56002 13.0803 2.5 12.929 2.5Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1,3 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.9999 11.0333C13.9999 11.3516 13.8735 11.6568 13.6484 11.8818C13.4234 12.1069 13.1182 12.2333 12.7999 12.2333H4.8966C4.57836 12.2334 4.27318 12.3599 4.04818 12.5849L2.72697 13.9061C2.66739 13.9657 2.59149 14.0063 2.50886 14.0227C2.42623 14.0391 2.34058 14.0307 2.26274 13.9985C2.18491 13.9662 2.11838 13.9116 2.07157 13.8416C2.02476 13.7715 1.99977 13.6892 1.99976 13.6049V3.8332C1.99976 3.51493 2.12619 3.2097 2.35123 2.98466C2.57628 2.75961 2.88151 2.63318 3.19977 2.63318H12.7999C13.1182 2.63318 13.4234 2.75961 13.6484 2.98466C13.8735 3.2097 13.9999 3.51493 13.9999 3.8332V11.0333Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M11.8889 2H4.11111C3.49746 2 3 2.59695 3 3.33333V12.6667C3 13.403 3.49746 14 4.11111 14H11.8889C12.5025 14 13 13.403 13 12.6667V3.33333C13 2.59695 12.5025 2 11.8889 2Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 10H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 566 B

Before After
Before After

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M2.998 3 13 13.002M6.174 3.345a5.001 5.001 0 0 1 6.476 6.481M11.54 11.542A5.008 5.008 0 0 1 4.458 4.46"/></svg> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2L14 14M5.81044 2.41392C6.89676 1.98976 8.08314 1.89138 9.22449 2.13079C10.3658 2.37021 11.4127 2.93705 12.237 3.76199C13.0613 4.58693 13.6273 5.6342 13.8658 6.77573C14.1044 7.91727 14.0051 9.10357 13.5801 10.1896M12.2484 12.2484C11.1176 13.3558 9.59562 13.9724 8.01292 13.9642C6.43021 13.956 4.91467 13.3236 3.79552 12.2045C2.67636 11.0853 2.044 9.56979 2.03578 7.98708C2.02757 6.40438 2.64417 4.88236 3.75165 3.75165" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

After

Width:  |  Height:  |  Size: 618 B

Before After
Before After

View file

@ -1,3 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.9999 11.0333C13.9999 11.3516 13.8735 11.6568 13.6484 11.8818C13.4234 12.1069 13.1182 12.2333 12.7999 12.2333H4.8966C4.57836 12.2334 4.27318 12.3599 4.04818 12.5849L2.72697 13.9061C2.66739 13.9657 2.59149 14.0063 2.50886 14.0227C2.42623 14.0391 2.34058 14.0307 2.26274 13.9985C2.18491 13.9662 2.11838 13.9116 2.07157 13.8416C2.02476 13.7715 1.99977 13.6892 1.99976 13.6049V3.8332C1.99976 3.51493 2.12619 3.2097 2.35123 2.98466C2.57628 2.75961 2.88151 2.63318 3.19977 2.63318H12.7999C13.1182 2.63318 13.4234 2.75961 13.6484 2.98466C13.8735 3.2097 13.9999 3.51493 13.9999 3.8332V11.0333Z" fill="black" fill-opacity="0.6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M11.8887 1.25C13.0386 1.25 13.7498 2.31634 13.75 3.33301V12.667C13.7499 13.6836 13.0386 14.75 11.8887 14.75H4.11133C2.96134 14.75 2.25014 13.6836 2.25 12.667V3.33301C2.25015 2.31635 2.96136 1.25 4.11133 1.25H11.8887ZM6 9.25C5.58579 9.25 5.25 9.58579 5.25 10C5.25 10.4142 5.58579 10.75 6 10.75H10C10.4142 10.75 10.75 10.4142 10.75 10C10.75 9.58579 10.4142 9.25 10 9.25H6ZM6 5.25C5.58579 5.25 5.25 5.58579 5.25 6C5.25 6.41421 5.58579 6.75 6 6.75H9C9.41421 6.75 9.75 6.41421 9.75 6C9.75 5.58579 9.41421 5.25 9 5.25H6Z" fill="black"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 643 B

Before After
Before After

View file

@ -1,5 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.0001 13.9999L12.7334 12.7333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M14.0001 13.9999L12.7334 12.7333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.3333 13.3334C12.4378 13.3334 13.3333 12.4379 13.3333 11.3334C13.3333 10.2288 12.4378 9.33337 11.3333 9.33337C10.2287 9.33337 9.33325 10.2288 9.33325 11.3334C9.33325 12.4379 10.2287 13.3334 11.3333 13.3334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/> <path d="M11.3333 13.3334C12.4378 13.3334 13.3333 12.4379 13.3333 11.3334C13.3333 10.2288 12.4378 9.33337 11.3333 9.33337C10.2287 9.33337 9.33325 10.2288 9.33325 11.3334C9.33325 12.4379 10.2287 13.3334 11.3333 13.3334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 12.5H3.6C3.30826 12.5 3.02847 12.3884 2.82218 12.1899C2.61589 11.9913 2.5 11.722 2.5 11.4412V4.55882C2.5 4.27801 2.61589 4.00869 2.82218 3.81012C3.02847 3.61155 3.30826 3.5 3.6 3.5H5.7615C5.94361 3.50003 6.12286 3.54358 6.28317 3.62674C6.44349 3.7099 6.57984 3.83007 6.68 3.97647L7.1255 4.61176C7.22668 4.75967 7.36478 4.88078 7.52717 4.96402C7.68955 5.04727 7.87103 5.08997 8.055 5.08824H12.4C12.6917 5.08824 12.9715 5.19979 13.1778 5.39836C13.3841 5.59693 13.5 5.86624 13.5 6.14706V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 13H3.2C2.88174 13 2.57651 12.8761 2.35148 12.6554C2.12643 12.4349 2 12.1356 2 11.8236V4.17647C2 3.86445 2.12643 3.56521 2.35148 3.34458C2.57651 3.12395 2.88174 3 3.2 3H5.558C5.75666 3.00004 5.95221 3.04842 6.1271 3.14082C6.30199 3.23322 6.45073 3.36675 6.56 3.52941L7.046 4.2353C7.15637 4.39964 7.30703 4.53421 7.48418 4.6267C7.66133 4.71919 7.8593 4.76664 8.06 4.76471H12.8C13.1183 4.76471 13.4235 4.88866 13.6486 5.10929C13.8735 5.32992 14 5.62916 14 5.94118V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

4
assets/icons/json.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.78125 3C3.90625 3 3.90625 4.5 3.90625 5.5C3.90625 6.5 3.40625 7.50106 2.40625 8C3.40625 8.50106 3.90625 9.5 3.90625 10.5C3.90625 11.5 3.90625 13 5.78125 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.2422 3C12.1172 3 12.1172 4.5 12.1172 5.5C12.1172 6.5 12.6172 7.50106 13.6172 8C12.6172 8.50106 12.1172 9.5 12.1172 10.5C12.1172 11.5 12.1172 13 10.2422 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 607 B

View file

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 3H13V6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10 3H13V6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 13H3V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6 13H3V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 3L9 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M13 3L9.5 6.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 13L7 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 525 B

Before After
Before After

View file

@ -1,6 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 9H7V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3.5 9.5H6.5V12.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 7H9V4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12.5 6.5H9.5V3.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 7L13 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M9.5 6.5L13 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 13L7 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M3 13L6.5 9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 539 B

Before After
Before After

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM5.413 5.413 8 8M13.333 2.667l-7.92 7.92M4 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM9.867 9.867l3.466 3.466"/></svg> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.03641 5.53641L8.33797 7.83797M13.0825 3.0934L6.03641 10.1395M9.99896 9.49896L13.0825 12.5825M4.77932 6.05864C5.25123 6.05864 5.7038 5.87118 6.03749 5.53749C6.37118 5.2038 6.55864 4.75123 6.55864 4.27932C6.55864 3.80742 6.37118 3.35484 6.03749 3.02115C5.7038 2.68746 5.25123 2.5 4.77932 2.5C4.30742 2.5 3.85484 2.68746 3.52115 3.02115C3.18746 3.35484 3 3.80742 3 4.27932C3 4.75123 3.18746 5.2038 3.52115 5.53749C3.85484 5.87118 4.30742 6.05864 4.77932 6.05864ZM4.77932 13.1759C5.25123 13.1759 5.7038 12.9885 6.03749 12.6548C6.37118 12.3211 6.55864 11.8685 6.55864 11.3966C6.55864 10.9247 6.37118 10.4721 6.03749 10.1384C5.7038 9.80475 5.25123 9.61729 4.77932 9.61729C4.30742 9.61729 3.85484 9.80475 3.52115 10.1384C3.18746 10.4721 3 10.9247 3 11.3966C3 11.8685 3.18746 12.3211 3.52115 12.6548C3.85484 12.9885 4.30742 13.1759 4.77932 13.1759Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

@ -239,6 +239,7 @@
"ctrl-shift-a": "agent::ToggleContextPicker", "ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-shift-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "assistant::QuoteSelection", "ctrl->": "assistant::QuoteSelection",
"ctrl-alt-e": "agent::RemoveAllContext", "ctrl-alt-e": "agent::RemoveAllContext",
@ -330,8 +331,6 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"enter": "agent::Chat", "enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff", "shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll", "ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll" "ctrl-shift-n": "agent::RejectAll"

View file

@ -279,6 +279,7 @@
"cmd-shift-a": "agent::ToggleContextPicker", "cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-shift-i": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor", "shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "assistant::QuoteSelection", "cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext", "cmd-alt-e": "agent::RemoveAllContext",
@ -382,8 +383,6 @@
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"enter": "agent::Chat", "enter": "agent::Chat",
"up": "agent::PreviousHistoryMessage",
"down": "agent::NextHistoryMessage",
"shift-ctrl-r": "agent::OpenAgentDiff", "shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll", "cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll" "cmd-shift-n": "agent::RejectAll"

View file

@ -58,6 +58,8 @@
"[ space": "vim::InsertEmptyLineAbove", "[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp", "[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown", "] e": "editor::MoveLineDown",
"[ f": "workspace::FollowNextCollaborator",
"] f": "workspace::FollowNextCollaborator",
// Word motions // Word motions
"w": "vim::NextWordStart", "w": "vim::NextWordStart",
@ -333,10 +335,14 @@
"ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific "ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific "ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
"ctrl-x ctrl-z": "editor::Cancel", "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",
"ctrl-u": "editor::DeleteToBeginningOfLine", "ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent", "ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent", "ctrl-d": "vim::Outdent",
"ctrl-y": "vim::InsertFromAbove",
"ctrl-e": "vim::InsertFromBelow",
"ctrl-k": ["vim::PushDigraph", {}], "ctrl-k": ["vim::PushDigraph", {}],
"ctrl-v": ["vim::PushLiteral", {}], "ctrl-v": ["vim::PushLiteral", {}],
"ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use. "ctrl-shift-v": "editor::Paste", // note: this is *very* similar to ctrl-v in vim, but ctrl-shift-v on linux is the typical shortcut for paste when ctrl-v is already in use.
@ -386,7 +392,7 @@
"right": "vim::WrappingRight", "right": "vim::WrappingRight",
"h": "vim::WrappingLeft", "h": "vim::WrappingLeft",
"l": "vim::WrappingRight", "l": "vim::WrappingRight",
"y": "editor::Copy", "y": "vim::HelixYank",
"alt-;": "vim::OtherEnd", "alt-;": "vim::OtherEnd",
"ctrl-r": "vim::Redo", "ctrl-r": "vim::Redo",
"f": ["vim::PushFindForward", { "before": false, "multiline": true }], "f": ["vim::PushFindForward", { "before": false, "multiline": true }],
@ -403,6 +409,7 @@
"g w": "vim::PushRewrap", "g w": "vim::PushRewrap",
"insert": "vim::InsertBefore", "insert": "vim::InsertBefore",
"alt-.": "vim::RepeatFind", "alt-.": "vim::RepeatFind",
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
// tree-sitter related commands // tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode", "[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode", "] x": "editor::SelectSmallerSyntaxNode",

View file

@ -28,7 +28,9 @@
"edit_prediction_provider": "zed" "edit_prediction_provider": "zed"
}, },
// The name of a font to use for rendering text in the editor // The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Plex Mono", // ".ZedMono" currently aliases to Lilex
// but this may change in the future.
"buffer_font_family": ".ZedMono",
// Set the buffer text's font fallbacks, this will be merged with // Set the buffer text's font fallbacks, this will be merged with
// the platform's default fallbacks. // the platform's default fallbacks.
"buffer_font_fallbacks": null, "buffer_font_fallbacks": null,
@ -54,7 +56,9 @@
"buffer_line_height": "comfortable", "buffer_line_height": "comfortable",
// The name of a font to use for rendering text in the UI // The name of a font to use for rendering text in the UI
// You can set this to ".SystemUIFont" to use the system font // You can set this to ".SystemUIFont" to use the system font
"ui_font_family": "Zed Plex Sans", // ".ZedSans" currently aliases to "IBM Plex Sans", but this may
// change in the future
"ui_font_family": ".ZedSans",
// Set the UI's font fallbacks, this will be merged with the platform's // Set the UI's font fallbacks, this will be merged with the platform's
// default font fallbacks. // default font fallbacks.
"ui_font_fallbacks": null, "ui_font_fallbacks": null,
@ -67,8 +71,8 @@
"ui_font_weight": 400, "ui_font_weight": 400,
// The default font size for text in the UI // The default font size for text in the UI
"ui_font_size": 16, "ui_font_size": 16,
// The default font size for text in the agent panel // The default font size for text in the agent panel. Falls back to the UI font size if unset.
"agent_font_size": 16, "agent_font_size": null,
// How much to fade out unused code. // How much to fade out unused code.
"unnecessary_code_fade": 0.3, "unnecessary_code_fade": 0.3,
// Active pane styling settings. // Active pane styling settings.
@ -82,10 +86,10 @@
// Layout mode of the bottom dock. Defaults to "contained" // Layout mode of the bottom dock. Defaults to "contained"
// choices: contained, full, left_aligned, right_aligned // choices: contained, full, left_aligned, right_aligned
"bottom_dock_layout": "contained", "bottom_dock_layout": "contained",
// The direction that you want to split panes horizontally. Defaults to "up" // The direction that you want to split panes horizontally. Defaults to "down"
"pane_split_direction_horizontal": "up", "pane_split_direction_horizontal": "down",
// The direction that you want to split panes vertically. Defaults to "left" // The direction that you want to split panes vertically. Defaults to "right"
"pane_split_direction_vertical": "left", "pane_split_direction_vertical": "right",
// Centered layout related settings. // Centered layout related settings.
"centered_layout": { "centered_layout": {
// The relative width of the left padding of the central pane from the // The relative width of the left padding of the central pane from the
@ -883,11 +887,6 @@
}, },
// The settings for slash commands. // The settings for slash commands.
"slash_commands": { "slash_commands": {
// Settings for the `/docs` slash command.
"docs": {
// Whether `/docs` is enabled.
"enabled": false
},
// Settings for the `/project` slash command. // Settings for the `/project` slash command.
"project": { "project": {
// Whether `/project` is enabled. // Whether `/project` is enabled.
@ -1252,7 +1251,9 @@
// Status bar-related settings. // Status bar-related settings.
"status_bar": { "status_bar": {
// Whether to show the active language button in the status bar. // Whether to show the active language button in the status bar.
"active_language_button": true "active_language_button": true,
// Whether to show the cursor position button in the status bar.
"cursor_position_button": true
}, },
// Settings specific to the terminal // Settings specific to the terminal
"terminal": { "terminal": {
@ -1402,7 +1403,7 @@
// "font_size": 15, // "font_size": 15,
// Set the terminal's font family. If this option is not included, // Set the terminal's font family. If this option is not included,
// the terminal will default to matching the buffer's font family. // the terminal will default to matching the buffer's font family.
// "font_family": "Zed Plex Mono", // "font_family": ".ZedMono",
// Set the terminal's font fallbacks. If this option is not included, // Set the terminal's font fallbacks. If this option is not included,
// the terminal will default to matching the buffer's font fallbacks. // the terminal will default to matching the buffer's font fallbacks.
// This will be merged with the platform's default font fallbacks // This will be merged with the platform's default font fallbacks

View file

@ -13,27 +13,35 @@ path = "src/acp_thread.rs"
doctest = false doctest = false
[features] [features]
test-support = ["gpui/test-support", "project/test-support"] test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
[dependencies] [dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true buffer_diff.workspace = true
collections.workspace = true
editor.workspace = true editor.workspace = true
file_icons.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true
markdown.workspace = true markdown.workspace = true
parking_lot = { workspace = true, optional = true }
project.workspace = true project.workspace = true
prompt_store.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
smol.workspace = true smol.workspace = true
terminal.workspace = true
ui.workspace = true ui.workspace = true
url.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true
watch.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
[dev-dependencies] [dev-dependencies]

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,98 @@
use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc}; use crate::AcpThread;
use agent_client_protocol::{self as acp}; use agent_client_protocol::{self as acp};
use anyhow::Result; use anyhow::Result;
use gpui::{AsyncApp, Entity, Task}; use collections::IndexMap;
use language_model::LanguageModel; use gpui::{Entity, SharedString, Task};
use project::Project; use project::Project;
use ui::App; use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
use ui::{App, IconName};
use uuid::Uuid;
use crate::AcpThread; #[derive(Clone, Debug, Eq, PartialEq)]
pub struct UserMessageId(Arc<str>);
impl UserMessageId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string().into())
}
}
pub trait AgentConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>>;
fn auth_methods(&self) -> &[acp::AuthMethod];
fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
fn prompt(
&self,
user_message_id: Option<UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>>;
fn resume(
&self,
_session_id: &acp::SessionId,
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionResume>> {
None
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
fn session_editor(
&self,
_session_id: &acp::SessionId,
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
None
}
/// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
///
/// If the agent does not support model selection, returns [None].
/// This allows sharing the selector in UI components.
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
impl dyn AgentConnection {
pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
self.into_any().downcast().ok()
}
}
pub trait AgentSessionEditor {
fn truncate(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
}
pub trait AgentSessionResume {
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
}
#[derive(Debug)]
pub struct AuthRequired;
impl Error for AuthRequired {}
impl fmt::Display for AuthRequired {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AuthRequired")
}
}
/// Trait for agents that support listing, selecting, and querying language models. /// Trait for agents that support listing, selecting, and querying language models.
/// ///
/// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. /// This is an optional capability; agents indicate support via [AgentConnection::model_selector].
pub trait ModelSelector: 'static { pub trait AgentModelSelector: 'static {
/// Lists all available language models for this agent. /// Lists all available language models for this agent.
/// ///
/// # Parameters /// # Parameters
@ -20,7 +100,7 @@ pub trait ModelSelector: 'static {
/// ///
/// # Returns /// # Returns
/// A task resolving to the list of models or an error (e.g., if no models are configured). /// A task resolving to the list of models or an error (e.g., if no models are configured).
fn list_models(&self, cx: &mut AsyncApp) -> Task<Result<Vec<Arc<dyn LanguageModel>>>>; fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>;
/// Selects a model for a specific session (thread). /// Selects a model for a specific session (thread).
/// ///
@ -37,8 +117,8 @@ pub trait ModelSelector: 'static {
fn select_model( fn select_model(
&self, &self,
session_id: acp::SessionId, session_id: acp::SessionId,
model: Arc<dyn LanguageModel>, model_id: AgentModelId,
cx: &mut AsyncApp, cx: &mut App,
) -> Task<Result<()>>; ) -> Task<Result<()>>;
/// Retrieves the currently selected model for a specific session (thread). /// Retrieves the currently selected model for a specific session (thread).
@ -52,42 +132,207 @@ pub trait ModelSelector: 'static {
fn selected_model( fn selected_model(
&self, &self,
session_id: &acp::SessionId, session_id: &acp::SessionId,
cx: &mut AsyncApp, cx: &mut App,
) -> Task<Result<Arc<dyn LanguageModel>>>; ) -> Task<Result<AgentModelInfo>>;
/// Whenever the model list is updated the receiver will be notified.
fn watch(&self, cx: &mut App) -> watch::Receiver<()>;
} }
pub trait AgentConnection { #[derive(Debug, Clone, PartialEq, Eq, Hash)]
fn new_thread( pub struct AgentModelId(pub SharedString);
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>>;
fn auth_methods(&self) -> &[acp::AuthMethod]; impl std::ops::Deref for AgentModelId {
type Target = SharedString;
fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>; fn deref(&self) -> &Self::Target {
&self.0
fn prompt(&self, params: acp::PromptRequest, cx: &mut App)
-> Task<Result<acp::PromptResponse>>;
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
/// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
///
/// If the agent does not support model selection, returns [None].
/// This allows sharing the selector in UI components.
fn model_selector(&self) -> Option<Rc<dyn ModelSelector>> {
None // Default impl for agents that don't support it
} }
} }
#[derive(Debug)] impl fmt::Display for AgentModelId {
pub struct AuthRequired;
impl Error for AuthRequired {}
impl fmt::Display for AuthRequired {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AuthRequired") self.0.fmt(f)
} }
} }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentModelInfo {
pub id: AgentModelId,
pub name: SharedString,
pub icon: Option<IconName>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AgentModelGroupName(pub SharedString);
#[derive(Debug, Clone)]
pub enum AgentModelList {
Flat(Vec<AgentModelInfo>),
Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),
}
impl AgentModelList {
pub fn is_empty(&self) -> bool {
match self {
AgentModelList::Flat(models) => models.is_empty(),
AgentModelList::Grouped(groups) => groups.is_empty(),
}
}
}
#[cfg(feature = "test-support")]
mod test_support {
use std::sync::Arc;
use collections::HashMap;
use futures::future::try_join_all;
use gpui::{AppContext as _, WeakEntity};
use parking_lot::Mutex;
use super::*;
#[derive(Clone, Default)]
pub struct StubAgentConnection {
sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
}
impl StubAgentConnection {
pub fn new() -> Self {
Self {
next_prompt_updates: Default::default(),
permission_requests: HashMap::default(),
sessions: Arc::default(),
}
}
pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
*self.next_prompt_updates.lock() = updates;
}
pub fn with_permission_requests(
mut self,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
) -> Self {
self.permission_requests = permission_requests;
self
}
pub fn send_update(
&self,
session_id: acp::SessionId,
update: acp::SessionUpdate,
cx: &mut App,
) {
self.sessions
.lock()
.get(&session_id)
.unwrap()
.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
})
.unwrap();
}
}
impl AgentConnection for StubAgentConnection {
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let thread =
cx.new(|cx| AcpThread::new("Test", self.clone(), project, session_id.clone(), cx));
self.sessions.lock().insert(session_id, thread.downgrade());
Task::ready(Ok(thread))
}
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
_cx: &mut App,
) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(
&self,
_id: Option<UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<gpui::Result<acp::PromptResponse>> {
let sessions = self.sessions.lock();
let thread = sessions.get(&params.session_id).unwrap();
let mut tasks = vec![];
for update in self.next_prompt_updates.lock().drain(..) {
let thread = thread.clone();
let update = update.clone();
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
&& let Some(options) = self.permission_requests.get(&tool_call.id)
{
Some((tool_call.clone(), options.clone()))
} else {
None
};
let task = cx.spawn(async move |cx| {
if let Some((tool_call, options)) = permission_request {
let permission = thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
cx,
)
})?;
permission?.await?;
}
thread.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
})?;
anyhow::Ok(())
});
tasks.push(task);
}
cx.spawn(async move |_| {
try_join_all(tasks).await?;
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
})
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
unimplemented!()
}
fn session_editor(
&self,
_session_id: &agent_client_protocol::SessionId,
_cx: &mut App,
) -> Option<Rc<dyn AgentSessionEditor>> {
Some(Rc::new(StubAgentSessionEditor))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct StubAgentSessionEditor;
impl AgentSessionEditor for StubAgentSessionEditor {
fn truncate(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
}
}
#[cfg(feature = "test-support")]
pub use test_support::*;

View file

@ -174,6 +174,10 @@ impl Diff {
buffer_text buffer_text
) )
} }
pub fn has_revealed_range(&self, cx: &App) -> bool {
self.multibuffer().read(cx).excerpt_paths().next().is_some()
}
} }
pub struct PendingDiff { pub struct PendingDiff {

View file

@ -0,0 +1,460 @@
use agent::ThreadId;
use anyhow::{Context as _, Result, bail};
use file_icons::FileIcons;
use prompt_store::{PromptId, UserPromptId};
use std::{
fmt,
ops::Range,
path::{Path, PathBuf},
str::FromStr,
};
use ui::{App, IconName, SharedString};
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MentionUri {
File {
abs_path: PathBuf,
is_directory: bool,
},
Symbol {
path: PathBuf,
name: String,
line_range: Range<u32>,
},
Thread {
id: ThreadId,
name: String,
},
TextThread {
path: PathBuf,
name: String,
},
Rule {
id: PromptId,
name: String,
},
Selection {
path: PathBuf,
line_range: Range<u32>,
},
Fetch {
url: Url,
},
}
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
if let Some(fragment) = url.fragment() {
let range = fragment
.strip_prefix("L")
.context("Line range must start with \"L\"")?;
let (start, end) = range
.split_once(":")
.context("Line range must use colon as separator")?;
let line_range = start
.parse::<u32>()
.context("Parsing line range start")?
.checked_sub(1)
.context("Line numbers should be 1-based")?
..end
.parse::<u32>()
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
path: path.into(),
line_range,
})
} else {
Ok(Self::Selection {
path: path.into(),
line_range,
})
}
} else {
let file_path =
PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
let is_directory = input.ends_with("/");
Ok(Self::File {
abs_path: file_path,
is_directory,
})
}
}
"zed" => {
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::Thread {
id: thread_id.into(),
name,
})
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::TextThread {
path: path.into(),
name,
})
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
let rule_id = UserPromptId(rule_id.parse()?);
Ok(Self::Rule {
id: rule_id.into(),
name,
})
} else {
bail!("invalid zed url: {:?}", input);
}
}
"http" | "https" => Ok(MentionUri::Fetch { url }),
other => bail!("unrecognized scheme {:?}", other),
}
}
pub fn name(&self) -> String {
match self {
MentionUri::File { abs_path, .. } => abs_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
path, line_range, ..
} => selection_name(path, line_range),
MentionUri::Fetch { url } => url.to_string(),
}
}
pub fn icon_path(&self, cx: &mut App) -> SharedString {
match self {
MentionUri::File {
abs_path,
is_directory,
} => {
if *is_directory {
FileIcons::get_folder_icon(false, cx)
.unwrap_or_else(|| IconName::Folder.path().into())
} else {
FileIcons::get_icon(&abs_path, cx)
.unwrap_or_else(|| IconName::File.path().into())
}
}
MentionUri::Symbol { .. } => IconName::Code.path().into(),
MentionUri::Thread { .. } => IconName::Thread.path().into(),
MentionUri::TextThread { .. } => IconName::Thread.path().into(),
MentionUri::Rule { .. } => IconName::Reader.path().into(),
MentionUri::Selection { .. } => IconName::Reader.path().into(),
MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
}
}
pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
MentionLink(self)
}
pub fn to_uri(&self) -> Url {
match self {
MentionUri::File {
abs_path,
is_directory,
} => {
let mut url = Url::parse("file:///").unwrap();
let mut path = abs_path.to_string_lossy().to_string();
if *is_directory && !path.ends_with("/") {
path.push_str("/");
}
url.set_path(&path);
url
}
MentionUri::Symbol {
path,
name,
line_range,
} => {
let mut url = Url::parse("file:///").unwrap();
url.set_path(&path.to_string_lossy());
url.query_pairs_mut().append_pair("symbol", name);
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start + 1,
line_range.end + 1
)));
url
}
MentionUri::Selection { path, line_range } => {
let mut url = Url::parse("file:///").unwrap();
url.set_path(&path.to_string_lossy());
url.set_fragment(Some(&format!(
"L{}:{}",
line_range.start + 1,
line_range.end + 1
)));
url
}
MentionUri::Thread { name, id } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/thread/{id}"));
url.query_pairs_mut().append_pair("name", name);
url
}
MentionUri::TextThread { path, name } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/text-thread/{}", path.to_string_lossy()));
url.query_pairs_mut().append_pair("name", name);
url
}
MentionUri::Rule { name, id } => {
let mut url = Url::parse("zed:///").unwrap();
url.set_path(&format!("/agent/rule/{id}"));
url.query_pairs_mut().append_pair("name", name);
url
}
MentionUri::Fetch { url } => url.clone(),
}
}
}
impl FromStr for MentionUri {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
Self::parse(s)
}
}
pub struct MentionLink<'a>(&'a MentionUri);
impl fmt::Display for MentionLink<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[@{}]({})", self.0.name(), self.0.to_uri())
}
}
fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
let pairs = url.query_pairs().collect::<Vec<_>>();
match pairs.as_slice() {
[] => Ok(None),
[(k, v)] => {
if k != name {
bail!("invalid query parameter")
}
Ok(Some(v.to_string()))
}
_ => bail!("too many query pairs"),
}
}
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
format!(
"{} ({}:{})",
path.file_name().unwrap_or_default().display(),
line_range.start + 1,
line_range.end + 1
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_file_uri() {
let file_uri = "file:///path/to/file.rs";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File {
abs_path,
is_directory,
} => {
assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs");
assert!(!is_directory);
}
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_parse_directory_uri() {
let file_uri = "file:///path/to/dir/";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File {
abs_path,
is_directory,
} => {
assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
assert!(is_directory);
}
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri().to_string(), file_uri);
}
#[test]
fn test_to_directory_uri_with_slash() {
let uri = MentionUri::File {
abs_path: PathBuf::from("/path/to/dir/"),
is_directory: true,
};
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
}
#[test]
fn test_to_directory_uri_without_slash() {
let uri = MentionUri::File {
abs_path: PathBuf::from("/path/to/dir"),
is_directory: true,
};
assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
}
#[test]
fn test_parse_symbol_uri() {
let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20";
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol {
path,
name,
line_range,
} => {
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
assert_eq!(name, "MySymbol");
assert_eq!(line_range.start, 9);
assert_eq!(line_range.end, 19);
}
_ => panic!("Expected Symbol variant"),
}
assert_eq!(parsed.to_uri().to_string(), symbol_uri);
}
#[test]
fn test_parse_selection_uri() {
let selection_uri = "file:///path/to/file.rs#L5:15";
let parsed = MentionUri::parse(selection_uri).unwrap();
match &parsed {
MentionUri::Selection { path, line_range } => {
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
assert_eq!(line_range.start, 4);
assert_eq!(line_range.end, 14);
}
_ => panic!("Expected Selection variant"),
}
assert_eq!(parsed.to_uri().to_string(), selection_uri);
}
#[test]
fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
let parsed = MentionUri::parse(thread_uri).unwrap();
match &parsed {
MentionUri::Thread {
id: thread_id,
name,
} => {
assert_eq!(thread_id.to_string(), "session123");
assert_eq!(name, "Thread name");
}
_ => panic!("Expected Thread variant"),
}
assert_eq!(parsed.to_uri().to_string(), thread_uri);
}
#[test]
fn test_parse_rule_uri() {
let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
let parsed = MentionUri::parse(rule_uri).unwrap();
match &parsed {
MentionUri::Rule { id, name } => {
assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
assert_eq!(name, "Some rule");
}
_ => panic!("Expected Rule variant"),
}
assert_eq!(parsed.to_uri().to_string(), rule_uri);
}
#[test]
fn test_parse_fetch_http_uri() {
let http_uri = "http://example.com/path?query=value#fragment";
let parsed = MentionUri::parse(http_uri).unwrap();
match &parsed {
MentionUri::Fetch { url } => {
assert_eq!(url.to_string(), http_uri);
}
_ => panic!("Expected Fetch variant"),
}
assert_eq!(parsed.to_uri().to_string(), http_uri);
}
#[test]
fn test_parse_fetch_https_uri() {
let https_uri = "https://example.com/api/endpoint";
let parsed = MentionUri::parse(https_uri).unwrap();
match &parsed {
MentionUri::Fetch { url } => {
assert_eq!(url.to_string(), https_uri);
}
_ => panic!("Expected Fetch variant"),
}
assert_eq!(parsed.to_uri().to_string(), https_uri);
}
#[test]
fn test_invalid_scheme() {
assert!(MentionUri::parse("ftp://example.com").is_err());
assert!(MentionUri::parse("ssh://example.com").is_err());
assert!(MentionUri::parse("unknown://example.com").is_err());
}
#[test]
fn test_invalid_zed_path() {
assert!(MentionUri::parse("zed:///invalid/path").is_err());
assert!(MentionUri::parse("zed:///agent/unknown/test").is_err());
}
#[test]
fn test_invalid_line_range_format() {
// Missing L prefix
assert!(MentionUri::parse("file:///path/to/file.rs#10:20").is_err());
// Missing colon separator
assert!(MentionUri::parse("file:///path/to/file.rs#L1020").is_err());
// Invalid numbers
assert!(MentionUri::parse("file:///path/to/file.rs#L10:abc").is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#Labc:20").is_err());
}
#[test]
fn test_invalid_query_parameters() {
// Invalid query parameter name
assert!(MentionUri::parse("file:///path/to/file.rs#L10:20?invalid=test").is_err());
// Too many query parameters
assert!(
MentionUri::parse("file:///path/to/file.rs#L10:20?symbol=test&another=param").is_err()
);
}
#[test]
fn test_zero_based_line_numbers() {
// Test that 0-based line numbers are rejected (should be 1-based)
assert!(MentionUri::parse("file:///path/to/file.rs#L0:10").is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L1:0").is_err());
assert!(MentionUri::parse("file:///path/to/file.rs#L0:0").is_err());
}
}

View file

@ -0,0 +1,93 @@
use gpui::{App, AppContext, Context, Entity};
use language::LanguageRegistry;
use markdown::Markdown;
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
pub struct Terminal {
command: Entity<Markdown>,
working_dir: Option<PathBuf>,
terminal: Entity<terminal::Terminal>,
started_at: Instant,
output: Option<TerminalOutput>,
}
pub struct TerminalOutput {
pub ended_at: Instant,
pub exit_status: Option<ExitStatus>,
pub was_content_truncated: bool,
pub original_content_len: usize,
pub content_line_count: usize,
pub finished_with_empty_output: bool,
}
impl Terminal {
pub fn new(
command: String,
working_dir: Option<PathBuf>,
terminal: Entity<terminal::Terminal>,
language_registry: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Self {
Self {
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
Some(language_registry.clone()),
None,
cx,
)
}),
working_dir,
terminal,
started_at: Instant::now(),
output: None,
}
}
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,
});
cx.notify();
}
pub fn command(&self) -> &Entity<Markdown> {
&self.command
}
pub fn working_dir(&self) -> &Option<PathBuf> {
&self.working_dir
}
pub fn started_at(&self) -> Instant {
self.started_at
}
pub fn output(&self) -> Option<&TerminalOutput> {
self.output.as_ref()
}
pub fn inner(&self) -> &Entity<terminal::Terminal> {
&self.terminal
}
pub fn to_markdown(&self, cx: &App) -> String {
format!(
"Terminal:\n```\n{}\n```\n",
self.terminal.read(cx).get_content()
)
}
}

View file

@ -0,0 +1,45 @@
[package]
name = "action_log"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lib]
path = "src/action_log.rs"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
buffer_diff.workspace = true
clock.workspace = true
collections.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
text.workspace = true
util.workspace = true
watch.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
buffer_diff = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
clock = { workspace = true, features = ["test-support"] }
ctor.workspace = true
gpui = { workspace = true, features = ["test-support"] }
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
log.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
zlog.workspace = true

View file

@ -17,8 +17,6 @@ use util::{
pub struct ActionLog { pub struct ActionLog {
/// Buffers that we want to notify the model about when they change. /// Buffers that we want to notify the model about when they change.
tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>, tracked_buffers: BTreeMap<Entity<Buffer>, TrackedBuffer>,
/// Has the model edited a file since it last checked diagnostics?
edited_since_project_diagnostics_check: bool,
/// The project this action log is associated with /// The project this action log is associated with
project: Entity<Project>, project: Entity<Project>,
} }
@ -28,7 +26,6 @@ impl ActionLog {
pub fn new(project: Entity<Project>) -> Self { pub fn new(project: Entity<Project>) -> Self {
Self { Self {
tracked_buffers: BTreeMap::default(), tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project, project,
} }
} }
@ -37,16 +34,6 @@ impl ActionLog {
&self.project &self.project
} }
/// Notifies a diagnostics check
pub fn checked_project_diagnostics(&mut self) {
self.edited_since_project_diagnostics_check = false;
}
/// Returns true if any files have been edited since the last project diagnostics check
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
self.edited_since_project_diagnostics_check
}
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> { pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
} }
@ -543,14 +530,11 @@ impl ActionLog {
/// Mark a buffer as created by agent, so we can refresh it in the context /// Mark a buffer as created by agent, so we can refresh it in the context
pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { pub fn buffer_created(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
self.track_buffer_internal(buffer.clone(), true, cx); self.track_buffer_internal(buffer.clone(), true, cx);
} }
/// Mark a buffer as edited by agent, so we can refresh it in the context /// Mark a buffer as edited by agent, so we can refresh it in the context
pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) { pub fn buffer_edited(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
self.edited_since_project_diagnostics_check = true;
let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status { if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified; tracked_buffer.status = TrackedBufferStatus::Modified;

View file

@ -716,18 +716,10 @@ impl ActivityIndicator {
})), })),
tooltip_message: Some(Self::version_tooltip_message(&version)), tooltip_message: Some(Self::version_tooltip_message(&version)),
}), }),
AutoUpdateStatus::Updated { AutoUpdateStatus::Updated { version } => Some(Content {
binary_path,
version,
} => Some(Content {
icon: None, icon: None,
message: "Click to restart and update Zed".to_string(), message: "Click to restart and update Zed".to_string(),
on_click: Some(Arc::new({ on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))),
let reload = workspace::Reload {
binary_path: Some(binary_path.clone()),
};
move |_, _, cx| workspace::reload(&reload, cx)
})),
tooltip_message: Some(Self::version_tooltip_message(&version)), tooltip_message: Some(Self::version_tooltip_message(&version)),
}), }),
AutoUpdateStatus::Errored => Some(Content { AutoUpdateStatus::Errored => Some(Content {

View file

@ -19,6 +19,7 @@ test-support = [
] ]
[dependencies] [dependencies]
action_log.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_context.workspace = true assistant_context.workspace = true

View file

@ -326,7 +326,7 @@ mod tests {
_input: serde_json::Value, _input: serde_json::Value,
_request: Arc<language_model::LanguageModelRequest>, _request: Arc<language_model::LanguageModelRequest>,
_project: Entity<Project>, _project: Entity<Project>,
_action_log: Entity<assistant_tool::ActionLog>, _action_log: Entity<action_log::ActionLog>,
_model: Arc<dyn language_model::LanguageModel>, _model: Arc<dyn language_model::LanguageModel>,
_window: Option<gpui::AnyWindowHandle>, _window: Option<gpui::AnyWindowHandle>,
_cx: &mut App, _cx: &mut App,

View file

@ -1,7 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use action_log::ActionLog;
use anyhow::{Result, anyhow, bail}; use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource}; use assistant_tool::{Tool, ToolResult, ToolSource};
use context_server::{ContextServerId, types}; use context_server::{ContextServerId, types};
use gpui::{AnyWindowHandle, App, Entity, Task}; use gpui::{AnyWindowHandle, App, Entity, Task};
use icons::IconName; use icons::IconName;

View file

@ -8,9 +8,10 @@ use crate::{
}, },
tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
}; };
use action_log::ActionLog;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage}; use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
@ -843,11 +844,17 @@ impl Thread {
.await .await
.unwrap_or(false); .unwrap_or(false);
if !equal { this.update(cx, |this, cx| {
this.update(cx, |this, cx| { this.pending_checkpoint = if equal {
this.insert_checkpoint(pending_checkpoint, cx) Some(pending_checkpoint)
})?; } else {
} this.insert_checkpoint(pending_checkpoint, cx);
Some(ThreadCheckpoint {
message_id: this.next_message_id,
git_checkpoint: final_checkpoint,
})
}
})?;
Ok(()) Ok(())
} }
@ -2267,6 +2274,15 @@ impl Thread {
max_attempts: 3, max_attempts: 3,
}) })
} }
Other(err)
if err.is::<PaymentRequiredError>()
|| err.is::<ModelRequestLimitReachedError>() =>
{
// Retrying won't help for Payment Required or Model Request Limit errors (where
// the user must upgrade to usage-based billing to get more requests, or else wait
// for a significant amount of time for the request limit to reset).
None
}
// Conservatively assume that any other errors are non-retryable // Conservatively assume that any other errors are non-retryable
HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY, delay: BASE_RETRY_DELAY,

View file

@ -205,6 +205,22 @@ impl ThreadStore {
(this, ready_rx) (this, ready_rx)
} }
#[cfg(any(test, feature = "test-support"))]
pub fn fake(project: Entity<Project>, cx: &mut App) -> Self {
Self {
project,
tools: cx.new(|_| ToolWorkingSet::default()),
prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()),
prompt_store: None,
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
project_context: SharedProjectContext::default(),
reload_system_prompt_tx: mpsc::channel(0).0,
_reload_system_prompt_task: Task::ready(()),
_subscriptions: vec![],
}
}
fn handle_project_event( fn handle_project_event(
&mut self, &mut self,
_project: Entity<Project>, _project: Entity<Project>,

View file

@ -1,9 +1,9 @@
[package] [package]
name = "agent2" name = "agent2"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
publish = false
[lib] [lib]
path = "src/agent2.rs" path = "src/agent2.rs"
@ -13,25 +13,32 @@ workspace = true
[dependencies] [dependencies]
acp_thread.workspace = true acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent_servers.workspace = true agent_servers.workspace = true
agent_settings.workspace = true agent_settings.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_tool.workspace = true assistant_tool.workspace = true
assistant_tools.workspace = true assistant_tools.workspace = true
chrono.workspace = true
cloud_llm_client.workspace = true cloud_llm_client.workspace = true
collections.workspace = true collections.workspace = true
context_server.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true
handlebars = { workspace = true, features = ["rust-embed"] } handlebars = { workspace = true, features = ["rust-embed"] }
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true indoc.workspace = true
itertools.workspace = true itertools.workspace = true
language.workspace = true language.workspace = true
language_model.workspace = true language_model.workspace = true
language_models.workspace = true language_models.workspace = true
log.workspace = true log.workspace = true
open.workspace = true
paths.workspace = true paths.workspace = true
portable-pty.workspace = true
project.workspace = true project.workspace = true
prompt_store.workspace = true prompt_store.workspace = true
rust-embed.workspace = true rust-embed.workspace = true
@ -40,16 +47,23 @@ serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
smol.workspace = true smol.workspace = true
task.workspace = true
terminal.workspace = true
text.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
watch.workspace = true watch.workspace = true
web_search.workspace = true
which.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
[dev-dependencies] [dev-dependencies]
ctor.workspace = true ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] } client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] } fs = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] }
@ -57,8 +71,14 @@ gpui_tokio.workspace = true
language = { workspace = true, "features" = ["test-support"] } language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] }
lsp = { workspace = true, "features" = ["test-support"] } lsp = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, "features" = ["test-support"] } project = { workspace = true, "features" = ["test-support"] }
reqwest_client.workspace = true reqwest_client.workspace = true
settings = { workspace = true, "features" = ["test-support"] } settings = { workspace = true, "features" = ["test-support"] }
tempfile.workspace = true
terminal = { workspace = true, "features" = ["test-support"] }
theme = { workspace = true, "features" = ["test-support"] }
tree-sitter-rust.workspace = true
unindent = { workspace = true }
worktree = { workspace = true, "features" = ["test-support"] } worktree = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true zlog.workspace = true

View file

@ -1,17 +1,27 @@
use crate::{templates::Templates, AgentResponseEvent, Thread}; use crate::{
use crate::{EditFileTool, FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization}; AgentResponseEvent, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool,
use acp_thread::ModelSelector; DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool,
MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread,
ToolCallAuthorization, UserMessageContent, WebSearchTool, templates::Templates,
};
use acp_thread::AgentModelSelector;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use anyhow::{anyhow, Context as _, Result}; use agent_settings::AgentSettings;
use futures::{future, StreamExt}; use anyhow::{Context as _, Result, anyhow};
use collections::{HashSet, IndexMap};
use fs::Fs;
use futures::channel::mpsc;
use futures::{StreamExt, future};
use gpui::{ use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
}; };
use language_model::{LanguageModel, LanguageModelRegistry}; use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree}; use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{ use prompt_store::{
ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
}; };
use settings::update_settings_file;
use std::any::Any;
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@ -44,6 +54,104 @@ struct Session {
_subscription: Subscription, _subscription: Subscription,
} }
pub struct LanguageModels {
/// Access language model by ID
models: HashMap<acp_thread::AgentModelId, Arc<dyn LanguageModel>>,
/// Cached list for returning language model information
model_list: acp_thread::AgentModelList,
refresh_models_rx: watch::Receiver<()>,
refresh_models_tx: watch::Sender<()>,
}
impl LanguageModels {
fn new(cx: &App) -> Self {
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
let mut this = Self {
models: HashMap::default(),
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
refresh_models_rx,
refresh_models_tx,
};
this.refresh_list(cx);
this
}
fn refresh_list(&mut self, cx: &App) {
let providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
let mut language_model_list = IndexMap::default();
let mut recommended_models = HashSet::default();
let mut recommended = Vec::new();
for provider in &providers {
for model in provider.recommended_models(cx) {
recommended_models.insert(model.id());
recommended.push(Self::map_language_model_to_info(&model, &provider));
}
}
if !recommended.is_empty() {
language_model_list.insert(
acp_thread::AgentModelGroupName("Recommended".into()),
recommended,
);
}
let mut models = HashMap::default();
for provider in providers {
let mut provider_models = Vec::new();
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
if !recommended_models.contains(&model.id()) {
provider_models.push(model_info);
}
models.insert(model_id, model);
}
if !provider_models.is_empty() {
language_model_list.insert(
acp_thread::AgentModelGroupName(provider.name().0.clone()),
provider_models,
);
}
}
self.models = models;
self.model_list = acp_thread::AgentModelList::Grouped(language_model_list);
self.refresh_models_tx.send(()).ok();
}
fn watch(&self) -> watch::Receiver<()> {
self.refresh_models_rx.clone()
}
pub fn model_from_id(
&self,
model_id: &acp_thread::AgentModelId,
) -> Option<Arc<dyn LanguageModel>> {
self.models.get(model_id).cloned()
}
fn map_language_model_to_info(
model: &Arc<dyn LanguageModel>,
provider: &Arc<dyn LanguageModelProvider>,
) -> acp_thread::AgentModelInfo {
acp_thread::AgentModelInfo {
id: Self::model_id(model),
name: model.name().0,
icon: Some(provider.icon()),
}
}
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
}
}
pub struct NativeAgent { pub struct NativeAgent {
/// Session ID -> Session mapping /// Session ID -> Session mapping
sessions: HashMap<acp::SessionId, Session>, sessions: HashMap<acp::SessionId, Session>,
@ -51,10 +159,14 @@ pub struct NativeAgent {
project_context: Rc<RefCell<ProjectContext>>, project_context: Rc<RefCell<ProjectContext>>,
project_context_needs_refresh: watch::Sender<()>, project_context_needs_refresh: watch::Sender<()>,
_maintain_project_context: Task<Result<()>>, _maintain_project_context: Task<Result<()>>,
context_server_registry: Entity<ContextServerRegistry>,
/// Shared templates for all threads /// Shared templates for all threads
templates: Arc<Templates>, templates: Arc<Templates>,
/// Cached model information
models: LanguageModels,
project: Entity<Project>, project: Entity<Project>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
fs: Arc<dyn Fs>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -63,6 +175,7 @@ impl NativeAgent {
project: Entity<Project>, project: Entity<Project>,
templates: Arc<Templates>, templates: Arc<Templates>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
fs: Arc<dyn Fs>,
cx: &mut AsyncApp, cx: &mut AsyncApp,
) -> Result<Entity<NativeAgent>> { ) -> Result<Entity<NativeAgent>> {
log::info!("Creating new NativeAgent"); log::info!("Creating new NativeAgent");
@ -72,7 +185,13 @@ impl NativeAgent {
.await; .await;
cx.new(|cx| { cx.new(|cx| {
let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)]; let mut subscriptions = vec![
cx.subscribe(&project, Self::handle_project_event),
cx.subscribe(
&LanguageModelRegistry::global(cx),
Self::handle_models_updated_event,
),
];
if let Some(prompt_store) = prompt_store.as_ref() { if let Some(prompt_store) = prompt_store.as_ref() {
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
} }
@ -86,14 +205,23 @@ impl NativeAgent {
_maintain_project_context: cx.spawn(async move |this, cx| { _maintain_project_context: cx.spawn(async move |this, cx| {
Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await
}), }),
context_server_registry: cx.new(|cx| {
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
}),
templates, templates,
models: LanguageModels::new(cx),
project, project,
prompt_store, prompt_store,
fs,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
}) })
} }
pub fn models(&self) -> &LanguageModels {
&self.models
}
async fn maintain_project_context( async fn maintain_project_context(
this: WeakEntity<Self>, this: WeakEntity<Self>,
mut needs_refresh: watch::Receiver<()>, mut needs_refresh: watch::Receiver<()>,
@ -289,211 +417,63 @@ impl NativeAgent {
) { ) {
self.project_context_needs_refresh.send(()).ok(); self.project_context_needs_refresh.send(()).ok();
} }
fn handle_models_updated_event(
&mut self,
_registry: Entity<LanguageModelRegistry>,
_event: &language_model::Event,
cx: &mut Context<Self>,
) {
self.models.refresh_list(cx);
for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, _| {
let model_id = LanguageModels::model_id(&thread.model());
if let Some(model) = self.models.model_from_id(&model_id) {
thread.set_model(model.clone());
}
});
}
}
} }
/// Wrapper struct that implements the AgentConnection trait /// Wrapper struct that implements the AgentConnection trait
#[derive(Clone)] #[derive(Clone)]
pub struct NativeAgentConnection(pub Entity<NativeAgent>); pub struct NativeAgentConnection(pub Entity<NativeAgent>);
impl ModelSelector for NativeAgentConnection { impl NativeAgentConnection {
fn list_models(&self, cx: &mut AsyncApp) -> Task<Result<Vec<Arc<dyn LanguageModel>>>> { pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option<Entity<Thread>> {
log::debug!("NativeAgentConnection::list_models called"); self.0
cx.spawn(async move |cx| { .read(cx)
cx.update(|cx| { .sessions
let registry = LanguageModelRegistry::read_global(cx); .get(session_id)
let models = registry.available_models(cx).collect::<Vec<_>>(); .map(|session| session.thread.clone())
log::info!("Found {} available models", models.len());
if models.is_empty() {
Err(anyhow::anyhow!("No models available"))
} else {
Ok(models)
}
})?
})
} }
fn select_model( fn run_turn(
&self, &self,
session_id: acp::SessionId, session_id: acp::SessionId,
model: Arc<dyn LanguageModel>,
cx: &mut AsyncApp,
) -> Task<Result<()>> {
log::info!(
"Setting model for session {}: {:?}",
session_id,
model.name()
);
let agent = self.0.clone();
cx.spawn(async move |cx| {
agent.update(cx, |agent, cx| {
if let Some(session) = agent.sessions.get(&session_id) {
session.thread.update(cx, |thread, _cx| {
thread.selected_model = model;
});
Ok(())
} else {
Err(anyhow!("Session not found"))
}
})?
})
}
fn selected_model(
&self,
session_id: &acp::SessionId,
cx: &mut AsyncApp,
) -> Task<Result<Arc<dyn LanguageModel>>> {
let agent = self.0.clone();
let session_id = session_id.clone();
cx.spawn(async move |cx| {
let thread = agent
.read_with(cx, |agent, _| {
agent
.sessions
.get(&session_id)
.map(|session| session.thread.clone())
})?
.ok_or_else(|| anyhow::anyhow!("Session not found"))?;
let selected = thread.read_with(cx, |thread, _| thread.selected_model.clone())?;
Ok(selected)
})
}
}
impl acp_thread::AgentConnection for NativeAgentConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone();
log::info!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context");
// Generate session ID
let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into());
log::info!("Created session with ID: {}", session_id);
// Create AcpThread
let acp_thread = cx.update(|cx| {
cx.new(|cx| {
acp_thread::AcpThread::new("agent2", self.clone(), project.clone(), session_id.clone(), cx)
})
})?;
let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
// Create Thread
let thread = agent.update(
cx,
|agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> {
// Fetch default model from registry settings
let registry = LanguageModelRegistry::read_global(cx);
// Log available models for debugging
let available_count = registry.available_models(cx).count();
log::debug!("Total available models: {}", available_count);
let default_model = registry
.default_model()
.map(|configured| {
log::info!(
"Using configured default model: {:?} from provider: {:?}",
configured.model.name(),
configured.provider.name()
);
configured.model
})
.ok_or_else(|| {
log::warn!("No default model configured in settings");
anyhow!("No default model configured. Please configure a default model in settings.")
})?;
let thread = cx.new(|cx| {
let mut thread = Thread::new(project.clone(), agent.project_context.clone(), action_log.clone(), agent.templates.clone(), default_model);
thread.add_tool(ThinkingTool);
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(EditFileTool::new(cx.entity()));
thread
});
Ok(thread)
},
)??;
// Store the session
agent.update(cx, |agent, cx| {
agent.sessions.insert(
session_id,
Session {
thread,
acp_thread: acp_thread.downgrade(),
_subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id());
})
},
);
})?;
Ok(acp_thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[] // No auth for in-process
}
fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn model_selector(&self) -> Option<Rc<dyn ModelSelector>> {
Some(Rc::new(self.clone()) as Rc<dyn ModelSelector>)
}
fn prompt(
&self,
params: acp::PromptRequest,
cx: &mut App, cx: &mut App,
f: impl 'static
+ FnOnce(
Entity<Thread>,
&mut App,
) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>>,
) -> Task<Result<acp::PromptResponse>> { ) -> Task<Result<acp::PromptResponse>> {
let session_id = params.session_id.clone(); let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| {
let agent = self.0.clone(); agent
log::info!("Received prompt request for session: {}", session_id); .sessions
log::debug!("Prompt blocks count: {}", params.prompt.len()); .get_mut(&session_id)
.map(|s| (s.thread.clone(), s.acp_thread.clone()))
}) else {
return Task::ready(Err(anyhow!("Session not found")));
};
log::debug!("Found session for: {}", session_id);
let mut response_stream = match f(thread, cx) {
Ok(stream) => stream,
Err(err) => return Task::ready(Err(err)),
};
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
// Get session
let (thread, acp_thread) = agent
.update(cx, |agent, _| {
agent
.sessions
.get_mut(&session_id)
.map(|s| (s.thread.clone(), s.acp_thread.clone()))
})?
.ok_or_else(|| {
log::error!("Session not found: {}", session_id);
anyhow::anyhow!("Session not found")
})?;
log::debug!("Found session for: {}", session_id);
// Convert prompt to message
let message = convert_prompt_to_message(params.prompt);
log::info!("Converted prompt to message: {} chars", message.len());
log::debug!("Message content: {}", message);
// Get model using the ModelSelector capability (always available for agent2)
// Get the selected model from the thread directly
let model = thread.read_with(cx, |thread, _| thread.selected_model.clone())?;
// Send to thread
log::info!("Sending message to thread with model: {:?}", model.name());
let mut response_stream =
thread.update(cx, |thread, cx| thread.send(model, message, cx))?;
// Handle response stream and forward to session.acp_thread // Handle response stream and forward to session.acp_thread
while let Some(result) = response_stream.next().await { while let Some(result) = response_stream.next().await {
match result { match result {
@ -534,10 +514,11 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
thread.request_tool_call_authorization(tool_call, options, cx) thread.request_tool_call_authorization(tool_call, options, cx)
})?; })?;
cx.background_spawn(async move { cx.background_spawn(async move {
if let Some(option) = recv if let Some(recv) = recv.log_err()
.await && let Some(option) = recv
.context("authorization sender was dropped") .await
.log_err() .context("authorization sender was dropped")
.log_err()
{ {
response response
.send(option) .send(option)
@ -550,7 +531,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
AgentResponseEvent::ToolCall(tool_call) => { AgentResponseEvent::ToolCall(tool_call) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
thread.upsert_tool_call(tool_call, cx) thread.upsert_tool_call(tool_call, cx)
})?; })??;
} }
AgentResponseEvent::ToolCallUpdate(update) => { AgentResponseEvent::ToolCallUpdate(update) => {
acp_thread.update(cx, |thread, cx| { acp_thread.update(cx, |thread, cx| {
@ -565,8 +546,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
} }
Err(e) => { Err(e) => {
log::error!("Error in model response stream: {:?}", e); log::error!("Error in model response stream: {:?}", e);
// TODO: Consider sending an error message to the UI return Err(e);
break;
} }
} }
} }
@ -577,6 +557,246 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}) })
}) })
} }
}
impl AgentModelSelector for NativeAgentConnection {
fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
log::debug!("NativeAgentConnection::list_models called");
let list = self.0.read(cx).models.model_list.clone();
Task::ready(if list.is_empty() {
Err(anyhow::anyhow!("No models available"))
} else {
Ok(list)
})
}
fn select_model(
&self,
session_id: acp::SessionId,
model_id: acp_thread::AgentModelId,
cx: &mut App,
) -> Task<Result<()>> {
log::info!("Setting model for session {}: {}", session_id, model_id);
let Some(thread) = self
.0
.read(cx)
.sessions
.get(&session_id)
.map(|session| session.thread.clone())
else {
return Task::ready(Err(anyhow!("Session not found")));
};
let Some(model) = self.0.read(cx).models.model_from_id(&model_id) else {
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
};
thread.update(cx, |thread, _cx| {
thread.set_model(model.clone());
});
update_settings_file::<AgentSettings>(
self.0.read(cx).fs.clone(),
cx,
move |settings, _cx| {
settings.set_model(model);
},
);
Task::ready(Ok(()))
}
fn selected_model(
&self,
session_id: &acp::SessionId,
cx: &mut App,
) -> Task<Result<acp_thread::AgentModelInfo>> {
let session_id = session_id.clone();
let Some(thread) = self
.0
.read(cx)
.sessions
.get(&session_id)
.map(|session| session.thread.clone())
else {
return Task::ready(Err(anyhow!("Session not found")));
};
let model = thread.read(cx).model().clone();
let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id())
else {
return Task::ready(Err(anyhow!("Provider not found")));
};
Task::ready(Ok(LanguageModels::map_language_model_to_info(
&model, &provider,
)))
}
fn watch(&self, cx: &mut App) -> watch::Receiver<()> {
self.0.read(cx).models.watch()
}
}
impl acp_thread::AgentConnection for NativeAgentConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<acp_thread::AcpThread>>> {
let agent = self.0.clone();
log::info!("Creating new thread for project at: {:?}", cwd);
cx.spawn(async move |cx| {
log::debug!("Starting thread creation in async context");
// Generate session ID
let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into());
log::info!("Created session with ID: {}", session_id);
// Create AcpThread
let acp_thread = cx.update(|cx| {
cx.new(|cx| {
acp_thread::AcpThread::new(
"agent2",
self.clone(),
project.clone(),
session_id.clone(),
cx,
)
})
})?;
let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
// Create Thread
let thread = agent.update(
cx,
|agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> {
// Fetch default model from registry settings
let registry = LanguageModelRegistry::read_global(cx);
// Log available models for debugging
let available_count = registry.available_models(cx).count();
log::debug!("Total available models: {}", available_count);
let default_model = registry
.default_model()
.and_then(|default_model| {
agent
.models
.model_from_id(&LanguageModels::model_id(&default_model.model))
})
.ok_or_else(|| {
log::warn!("No default model configured in settings");
anyhow!(
"No default model. Please configure a default model in settings."
)
})?;
let thread = cx.new(|cx| {
let mut thread = Thread::new(
project.clone(),
agent.project_context.clone(),
agent.context_server_registry.clone(),
action_log.clone(),
agent.templates.clone(),
default_model,
cx,
);
thread.add_tool(CopyPathTool::new(project.clone()));
thread.add_tool(CreateDirectoryTool::new(project.clone()));
thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone()));
thread.add_tool(DiagnosticsTool::new(project.clone()));
thread.add_tool(EditFileTool::new(cx.entity()));
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(GrepTool::new(project.clone()));
thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(MovePathTool::new(project.clone()));
thread.add_tool(NowTool);
thread.add_tool(OpenTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(TerminalTool::new(project.clone(), cx));
thread.add_tool(ThinkingTool);
thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model.
thread
});
Ok(thread)
},
)??;
// Store the session
agent.update(cx, |agent, cx| {
agent.sessions.insert(
session_id,
Session {
thread,
acp_thread: acp_thread.downgrade(),
_subscription: cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id());
}),
},
);
})?;
Ok(acp_thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[] // No auth for in-process
}
fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
Some(Rc::new(self.clone()) as Rc<dyn AgentModelSelector>)
}
fn prompt(
&self,
id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let id = id.expect("UserMessageId is required");
let session_id = params.session_id.clone();
log::info!("Received prompt request for session: {}", session_id);
log::debug!("Prompt blocks count: {}", params.prompt.len());
self.run_turn(session_id, cx, |thread, cx| {
let content: Vec<UserMessageContent> = params
.prompt
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
log::info!("Converted prompt to message: {} chars", content.len());
log::debug!("Message id: {:?}", id);
log::debug!("Message content: {:?}", content);
Ok(thread.update(cx, |thread, cx| {
log::info!(
"Sending message to thread with model: {:?}",
thread.model().name()
);
thread.send(id, content, cx)
}))
})
}
fn resume(
&self,
session_id: &acp::SessionId,
_cx: &mut App,
) -> Option<Rc<dyn acp_thread::AgentSessionResume>> {
Some(Rc::new(NativeAgentSessionResume {
connection: self.clone(),
session_id: session_id.clone(),
}) as _)
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
log::info!("Cancelling on session: {}", session_id); log::info!("Cancelling on session: {}", session_id);
@ -586,44 +806,51 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
} }
}); });
} }
}
/// Convert ACP content blocks to a message string fn session_editor(
fn convert_prompt_to_message(blocks: Vec<acp::ContentBlock>) -> String { &self,
log::debug!("Converting {} content blocks to message", blocks.len()); session_id: &agent_client_protocol::SessionId,
let mut message = String::new(); cx: &mut App,
) -> Option<Rc<dyn acp_thread::AgentSessionEditor>> {
for block in blocks { self.0.update(cx, |agent, _cx| {
match block { agent
acp::ContentBlock::Text(text) => { .sessions
log::trace!("Processing text block: {} chars", text.text.len()); .get(session_id)
message.push_str(&text.text); .map(|session| Rc::new(NativeAgentSessionEditor(session.thread.clone())) as _)
} })
acp::ContentBlock::ResourceLink(link) => {
log::trace!("Processing resource link: {}", link.uri);
message.push_str(&format!(" @{} ", link.uri));
}
acp::ContentBlock::Image(_) => {
log::trace!("Processing image block");
message.push_str(" [image] ");
}
acp::ContentBlock::Audio(_) => {
log::trace!("Processing audio block");
message.push_str(" [audio] ");
}
acp::ContentBlock::Resource(resource) => {
log::trace!("Processing resource block: {:?}", resource.resource);
message.push_str(&format!(" [resource: {:?}] ", resource.resource));
}
}
} }
message fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct NativeAgentSessionEditor(Entity<Thread>);
impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
Task::ready(self.0.update(cx, |thread, _cx| thread.truncate(message_id)))
}
}
struct NativeAgentSessionResume {
connection: NativeAgentConnection,
session_id: acp::SessionId,
}
impl acp_thread::AgentSessionResume for NativeAgentSessionResume {
fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>> {
self.connection
.run_turn(self.session_id.clone(), cx, |thread, cx| {
thread.update(cx, |thread, cx| thread.resume(cx))
})
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo};
use fs::FakeFs; use fs::FakeFs;
use gpui::TestAppContext; use gpui::TestAppContext;
use serde_json::json; use serde_json::json;
@ -641,9 +868,15 @@ mod tests {
) )
.await; .await;
let project = Project::test(fs.clone(), [], cx).await; let project = Project::test(fs.clone(), [], cx).await;
let agent = NativeAgent::new(project.clone(), Templates::new(), None, &mut cx.to_async()) let agent = NativeAgent::new(
.await project.clone(),
.unwrap(); Templates::new(),
None,
fs.clone(),
&mut cx.to_async(),
)
.await
.unwrap();
agent.read_with(cx, |agent, _| { agent.read_with(cx, |agent, _| {
assert_eq!(agent.project_context.borrow().worktrees, vec![]) assert_eq!(agent.project_context.borrow().worktrees, vec![])
}); });
@ -684,13 +917,127 @@ mod tests {
}); });
} }
#[gpui::test]
async fn test_listing_models(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/", json!({ "a": {} })).await;
let project = Project::test(fs.clone(), [], cx).await;
let connection = NativeAgentConnection(
NativeAgent::new(
project.clone(),
Templates::new(),
None,
fs.clone(),
&mut cx.to_async(),
)
.await
.unwrap(),
);
let models = cx.update(|cx| connection.list_models(cx)).await.unwrap();
let acp_thread::AgentModelList::Grouped(models) = models else {
panic!("Unexpected model group");
};
assert_eq!(
models,
IndexMap::from_iter([(
AgentModelGroupName("Fake".into()),
vec![AgentModelInfo {
id: AgentModelId("fake/fake".into()),
name: "Fake".into(),
icon: Some(ui::IconName::ZedAssistant),
}]
)])
);
}
#[gpui::test]
async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
fs.insert_file(
paths::settings_file(),
json!({
"agent": {
"default_model": {
"provider": "foo",
"model": "bar"
}
}
})
.to_string()
.into_bytes(),
)
.await;
let project = Project::test(fs.clone(), [], cx).await;
// Create the agent and connection
let agent = NativeAgent::new(
project.clone(),
Templates::new(),
None,
fs.clone(),
&mut cx.to_async(),
)
.await
.unwrap();
let connection = NativeAgentConnection(agent.clone());
// Create a thread/session
let acp_thread = cx
.update(|cx| {
Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx)
})
.await
.unwrap();
let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
// Select a model
let model_id = AgentModelId("fake/fake".into());
cx.update(|cx| connection.select_model(session_id.clone(), model_id.clone(), cx))
.await
.unwrap();
// Verify the thread has the selected model
agent.read_with(cx, |agent, _| {
let session = agent.sessions.get(&session_id).unwrap();
session.thread.read_with(cx, |thread, _| {
assert_eq!(thread.model().id().0, "fake");
});
});
cx.run_until_parked();
// Verify settings file was updated
let settings_content = fs.load(paths::settings_file()).await.unwrap();
let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap();
// Check that the agent settings contain the selected model
assert_eq!(
settings_json["agent"]["default_model"]["model"],
json!("fake")
);
assert_eq!(
settings_json["agent"]["default_model"]["provider"],
json!("fake")
);
}
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok(); env_logger::try_init().ok();
cx.update(|cx| { cx.update(|cx| {
let settings_store = SettingsStore::test(cx); let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store); cx.set_global(settings_store);
Project::init_settings(cx); Project::init_settings(cx);
agent_settings::init(cx);
language::init(cx); language::init(cx);
LanguageModelRegistry::test(cx);
}); });
} }
} }

View file

@ -1,16 +1,24 @@
use std::path::Path; use std::{path::Path, rc::Rc, sync::Arc};
use std::rc::Rc;
use agent_servers::AgentServer; use agent_servers::AgentServer;
use anyhow::Result; use anyhow::Result;
use fs::Fs;
use gpui::{App, Entity, Task}; use gpui::{App, Entity, Task};
use project::Project; use project::Project;
use prompt_store::PromptStore; use prompt_store::PromptStore;
use crate::{templates::Templates, NativeAgent, NativeAgentConnection}; use crate::{NativeAgent, NativeAgentConnection, templates::Templates};
#[derive(Clone)] #[derive(Clone)]
pub struct NativeAgentServer; pub struct NativeAgentServer {
fs: Arc<dyn Fs>,
}
impl NativeAgentServer {
pub fn new(fs: Arc<dyn Fs>) -> Self {
Self { fs }
}
}
impl AgentServer for NativeAgentServer { impl AgentServer for NativeAgentServer {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
@ -41,6 +49,7 @@ impl AgentServer for NativeAgentServer {
_root_dir _root_dir
); );
let project = project.clone(); let project = project.clone();
let fs = self.fs.clone();
let prompt_store = PromptStore::global(cx); let prompt_store = PromptStore::global(cx);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
log::debug!("Creating templates for native agent"); log::debug!("Creating templates for native agent");
@ -48,7 +57,7 @@ impl AgentServer for NativeAgentServer {
let prompt_store = prompt_store.await?; let prompt_store = prompt_store.await?;
log::debug!("Creating native agent entity"); log::debug!("Creating native agent entity");
let agent = NativeAgent::new(project, templates, Some(prompt_store), cx).await?; let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?;
// Create the connection wrapper // Create the connection wrapper
let connection = NativeAgentConnection(agent); let connection = NativeAgentConnection(agent);

File diff suppressed because it is too large Load diff

View file

@ -7,7 +7,7 @@ use std::future;
#[derive(JsonSchema, Serialize, Deserialize)] #[derive(JsonSchema, Serialize, Deserialize)]
pub struct EchoToolInput { pub struct EchoToolInput {
/// The text to echo. /// The text to echo.
text: String, pub text: String,
} }
pub struct EchoTool; pub struct EchoTool;
@ -110,9 +110,9 @@ impl AgentTool for ToolRequiringPermission {
event_stream: ToolCallEventStream, event_stream: ToolCallEventStream,
cx: &mut App, cx: &mut App,
) -> Task<Result<String>> { ) -> Task<Result<String>> {
let auth_check = event_stream.authorize("Authorize?".into()); let authorize = event_stream.authorize("Authorize?", cx);
cx.foreground_executor().spawn(async move { cx.foreground_executor().spawn(async move {
auth_check.await?; authorize.await?;
Ok("Allowed".to_string()) Ok("Allowed".to_string())
}) })
} }

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,35 @@
mod context_server_registry;
mod copy_path_tool;
mod create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool; mod edit_file_tool;
mod fetch_tool;
mod find_path_tool; mod find_path_tool;
mod grep_tool;
mod list_directory_tool;
mod move_path_tool;
mod now_tool;
mod open_tool;
mod read_file_tool; mod read_file_tool;
mod terminal_tool;
mod thinking_tool; mod thinking_tool;
mod web_search_tool;
pub use context_server_registry::*;
pub use copy_path_tool::*;
pub use create_directory_tool::*;
pub use delete_path_tool::*;
pub use diagnostics_tool::*;
pub use edit_file_tool::*; pub use edit_file_tool::*;
pub use fetch_tool::*;
pub use find_path_tool::*; pub use find_path_tool::*;
pub use grep_tool::*;
pub use list_directory_tool::*;
pub use move_path_tool::*;
pub use now_tool::*;
pub use open_tool::*;
pub use read_file_tool::*; pub use read_file_tool::*;
pub use terminal_tool::*;
pub use thinking_tool::*; pub use thinking_tool::*;
pub use web_search_tool::*;

View file

@ -0,0 +1,231 @@
use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Result, anyhow, bail};
use collections::{BTreeMap, HashMap};
use context_server::ContextServerId;
use gpui::{App, Context, Entity, SharedString, Task};
use project::context_server_store::{ContextServerStatus, ContextServerStore};
use std::sync::Arc;
use util::ResultExt;
pub struct ContextServerRegistry {
server_store: Entity<ContextServerStore>,
registered_servers: HashMap<ContextServerId, RegisteredContextServer>,
_subscription: gpui::Subscription,
}
struct RegisteredContextServer {
tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
load_tools: Task<Result<()>>,
}
impl ContextServerRegistry {
pub fn new(server_store: Entity<ContextServerStore>, cx: &mut Context<Self>) -> Self {
let mut this = Self {
server_store: server_store.clone(),
registered_servers: HashMap::default(),
_subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event),
};
for server in server_store.read(cx).running_servers() {
this.reload_tools_for_server(server.id(), cx);
}
this
}
pub fn servers(
&self,
) -> impl Iterator<
Item = (
&ContextServerId,
&BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
),
> {
self.registered_servers
.iter()
.map(|(id, server)| (id, &server.tools))
}
fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else {
return;
};
let Some(client) = server.client() else {
return;
};
if !client.capable(context_server::protocol::ServerCapability::Tools) {
return;
}
let registered_server =
self.registered_servers
.entry(server_id.clone())
.or_insert(RegisteredContextServer {
tools: BTreeMap::default(),
load_tools: Task::ready(Ok(())),
});
registered_server.load_tools = cx.spawn(async move |this, cx| {
let response = client
.request::<context_server::types::requests::ListTools>(())
.await;
this.update(cx, |this, cx| {
let Some(registered_server) = this.registered_servers.get_mut(&server_id) else {
return;
};
registered_server.tools.clear();
if let Some(response) = response.log_err() {
for tool in response.tools {
let tool = Arc::new(ContextServerTool::new(
this.server_store.clone(),
server.id(),
tool,
));
registered_server.tools.insert(tool.name(), tool);
}
cx.notify();
}
})
});
}
fn handle_context_server_store_event(
&mut self,
_: Entity<ContextServerStore>,
event: &project::context_server_store::Event,
cx: &mut Context<Self>,
) {
match event {
project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
match status {
ContextServerStatus::Starting => {}
ContextServerStatus::Running => {
self.reload_tools_for_server(server_id.clone(), cx);
}
ContextServerStatus::Stopped | ContextServerStatus::Error(_) => {
self.registered_servers.remove(&server_id);
cx.notify();
}
}
}
}
}
}
struct ContextServerTool {
store: Entity<ContextServerStore>,
server_id: ContextServerId,
tool: context_server::types::Tool,
}
impl ContextServerTool {
fn new(
store: Entity<ContextServerStore>,
server_id: ContextServerId,
tool: context_server::types::Tool,
) -> Self {
Self {
store,
server_id,
tool,
}
}
}
impl AnyAgentTool for ContextServerTool {
fn name(&self) -> SharedString {
self.tool.name.clone().into()
}
fn description(&self) -> SharedString {
self.tool.description.clone().unwrap_or_default().into()
}
fn kind(&self) -> ToolKind {
ToolKind::Other
}
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
format!("Run MCP tool `{}`", self.tool.name).into()
}
fn input_schema(
&self,
format: language_model::LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone();
assistant_tool::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })
}
serde_json::Value::Object(map) if map.is_empty() => {
serde_json::json!({ "type": "object", "properties": [] })
}
_ => schema,
})
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<AgentToolOutput>> {
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
let server_clone = server.clone();
let input_clone = input.clone();
cx.spawn(async move |_cx| {
let Some(protocol) = server_clone.client() else {
bail!("Context server not initialized");
};
let arguments = if let serde_json::Value::Object(map) = input_clone {
Some(map.into_iter().collect())
} else {
None
};
log::trace!(
"Running tool: {} with arguments: {:?}",
tool_name,
arguments
);
let response = protocol
.request::<context_server::types::requests::CallTool>(
context_server::types::CallToolParams {
name: tool_name,
arguments,
meta: None,
},
)
.await?;
let mut result = String::new();
for content in response.content {
match content {
context_server::types::ToolResponseContent::Text { text } => {
result.push_str(&text);
}
context_server::types::ToolResponseContent::Image { .. } => {
log::warn!("Ignoring image content from tool response");
}
context_server::types::ToolResponseContent::Audio { .. } => {
log::warn!("Ignoring audio content from tool response");
}
context_server::types::ToolResponseContent::Resource { .. } => {
log::warn!("Ignoring resource content from tool response");
}
}
}
Ok(AgentToolOutput {
raw_output: result.clone().into(),
llm_output: result.into(),
})
})
}
}

View file

@ -0,0 +1,118 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::markdown::MarkdownInlineCode;
/// Copies a file or directory in the project, and returns confirmation that the
/// copy succeeded.
///
/// Directory contents will be copied recursively (like `cp -r`).
///
/// This tool should be used when it's desirable to create a copy of a file or
/// directory without modifying the original. It's much more efficient than
/// doing this by separately reading and then writing the file or directory's
/// contents, so this tool should be preferred over that approach whenever
/// copying is the goal.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CopyPathToolInput {
/// The source path of the file or directory to copy.
/// If a directory is specified, its contents will be copied recursively (like `cp -r`).
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can copy the first file by providing a source_path of "directory1/a/something.txt"
/// </example>
pub source_path: String,
/// The destination path where the file or directory should be copied to.
///
/// <example>
/// To copy "directory1/a/something.txt" to "directory2/b/copy.txt",
/// provide a destination_path of "directory2/b/copy.txt"
/// </example>
pub destination_path: String,
}
pub struct CopyPathTool {
project: Entity<Project>,
}
impl CopyPathTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for CopyPathTool {
type Input = CopyPathToolInput;
type Output = String;
fn name(&self) -> SharedString {
"copy_path".into()
}
fn kind(&self) -> ToolKind {
ToolKind::Move
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
format!("Copy {src} to {dest}").into()
} else {
"Copy path".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let copy_task = self.project.update(cx, |project, cx| {
match project
.find_project_path(&input.source_path, cx)
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => {
project.copy_entry(entity.id, None, project_path.path, cx)
}
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
))),
},
None => Task::ready(Err(anyhow!(
"Source path {} was not found in the project.",
input.source_path
))),
}
});
cx.background_spawn(async move {
let _ = copy_task.await.with_context(|| {
format!(
"Copying {} to {}",
input.source_path, input.destination_path
)
})?;
Ok(format!(
"Copied {} to {}",
input.source_path, input.destination_path
))
})
}
}

View file

@ -0,0 +1,89 @@
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use util::markdown::MarkdownInlineCode;
use crate::{AgentTool, ToolCallEventStream};
/// Creates a new directory at the specified path within the project. Returns
/// confirmation that the directory was created.
///
/// This tool creates a directory and all necessary parent directories (similar
/// to `mkdir -p`). It should be used whenever you need to create new
/// directories within the project.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateDirectoryToolInput {
/// The path of the new directory.
///
/// <example>
/// If the project has the following structure:
///
/// - directory1/
/// - directory2/
///
/// You can create a new directory by providing a path of "directory1/new_directory"
/// </example>
pub path: String,
}
pub struct CreateDirectoryTool {
project: Entity<Project>,
}
impl CreateDirectoryTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for CreateDirectoryTool {
type Input = CreateDirectoryToolInput;
type Output = String;
fn name(&self) -> SharedString {
"create_directory".into()
}
fn kind(&self) -> ToolKind {
ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
} else {
"Create directory".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let project_path = match self.project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => {
return Task::ready(Err(anyhow!("Path to create was outside the project")));
}
};
let destination_path: Arc<str> = input.path.as_str().into();
let create_entry = self.project.update(cx, |project, cx| {
project.create_entry(project_path.clone(), true, cx)
});
cx.spawn(async move |_cx| {
create_entry
.await
.with_context(|| format!("Creating directory {destination_path}"))?;
Ok(format!("Created directory {destination_path}"))
})
}
}

View file

@ -0,0 +1,137 @@
use crate::{AgentTool, ToolCallEventStream};
use action_log::ActionLog;
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Deletes the file or directory (and the directory's contents, recursively) at
/// the specified path in the project, and returns confirmation of the deletion.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeletePathToolInput {
/// The path of the file or directory to delete.
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can delete the first file by providing a path of "directory1/a/something.txt"
/// </example>
pub path: String,
}
pub struct DeletePathTool {
project: Entity<Project>,
action_log: Entity<ActionLog>,
}
impl DeletePathTool {
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
Self {
project,
action_log,
}
}
}
impl AgentTool for DeletePathTool {
type Input = DeletePathToolInput;
type Output = String;
fn name(&self) -> SharedString {
"delete_path".into()
}
fn kind(&self) -> ToolKind {
ToolKind::Delete
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Delete “`{}`”", input.path).into()
} else {
"Delete path".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let path = input.path;
let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path} because that path isn't in this project."
)));
};
let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!(
"Couldn't delete {path} because that path isn't in this project."
)));
};
let worktree_snapshot = worktree.read(cx).snapshot();
let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
cx.background_spawn({
let project_path = project_path.clone();
async move {
for entry in
worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
{
if !entry.path.starts_with(&project_path.path) {
break;
}
paths_tx
.send(ProjectPath {
worktree_id: project_path.worktree_id,
path: entry.path.clone(),
})
.await?;
}
anyhow::Ok(())
}
})
.detach();
let project = self.project.clone();
let action_log = self.action_log.clone();
cx.spawn(async move |cx| {
while let Some(path) = paths_rx.next().await {
if let Ok(buffer) = project
.update(cx, |project, cx| project.open_buffer(path, cx))?
.await
{
action_log.update(cx, |action_log, cx| {
action_log.will_delete_buffer(buffer.clone(), cx)
})?;
}
}
let deletion_task = project
.update(cx, |project, cx| {
project.delete_file(project_path, false, cx)
})?
.with_context(|| {
format!("Couldn't delete {path} because that path isn't in this project.")
})?;
deletion_task
.await
.with_context(|| format!("Deleting {path}"))?;
Ok(format!("Deleted {path}"))
})
}
}

View file

@ -0,0 +1,163 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use gpui::{App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{fmt::Write, path::Path, sync::Arc};
use ui::SharedString;
use util::markdown::MarkdownInlineCode;
/// Get errors and warnings for the project or a specific file.
///
/// This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase.
///
/// When a path is provided, shows all diagnostics for that specific file.
/// When no path is provided, shows a summary of error and warning counts for all files in the project.
///
/// <example>
/// To get diagnostics for a specific file:
/// {
/// "path": "src/main.rs"
/// }
///
/// To get a project-wide diagnostic summary:
/// {}
/// </example>
///
/// <guidelines>
/// - If you think you can fix a diagnostic, make 1-2 attempts and then give up.
/// - Don't remove code you've generated just because you can't fix an error. The user can help you fix it.
/// </guidelines>
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DiagnosticsToolInput {
/// The path to get diagnostics for. If not provided, returns a project-wide summary.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - lorem
/// - ipsum
///
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
/// </example>
pub path: Option<String>,
}
pub struct DiagnosticsTool {
project: Entity<Project>,
}
impl DiagnosticsTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for DiagnosticsTool {
type Input = DiagnosticsToolInput;
type Output = String;
fn name(&self) -> SharedString {
"diagnostics".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Some(path) = input.ok().and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(path),
_ => None,
}) {
format!("Check diagnostics for {}", MarkdownInlineCode(&path)).into()
} else {
"Check project diagnostics".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
match input.path {
Some(path) if !path.is_empty() => {
let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else {
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
};
let buffer = self
.project
.update(cx, |project, cx| project.open_buffer(project_path, cx));
cx.spawn(async move |cx| {
let mut output = String::new();
let buffer = buffer.await?;
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
for (_, group) in snapshot.diagnostic_groups(None) {
let entry = &group.entries[group.primary_ix];
let range = entry.range.to_point(&snapshot);
let severity = match entry.diagnostic.severity {
DiagnosticSeverity::ERROR => "error",
DiagnosticSeverity::WARNING => "warning",
_ => continue,
};
writeln!(
output,
"{} at line {}: {}",
severity,
range.start.row + 1,
entry.diagnostic.message
)?;
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
} else {
Ok(output)
}
})
}
_ => {
let project = self.project.read(cx);
let mut output = String::new();
let mut has_diagnostics = false;
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
if summary.error_count > 0 || summary.warning_count > 0 {
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
else {
continue;
};
has_diagnostics = true;
output.push_str(&format!(
"{}: {} error(s), {} warning(s)\n",
Path::new(worktree.read(cx).root_name())
.join(project_path.path)
.display(),
summary.error_count,
summary.warning_count
));
}
}
if has_diagnostics {
Task::ready(Ok(output))
} else {
Task::ready(Ok("No errors or warnings found in the project.".into()))
}
}
}
}
}

View file

@ -1,12 +1,13 @@
use crate::{AgentTool, Thread, ToolCallEventStream}; use crate::{AgentTool, Thread, ToolCallEventStream};
use acp_thread::Diff; use acp_thread::Diff;
use agent_client_protocol as acp; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields};
use anyhow::{anyhow, Context as _, Result}; use anyhow::{Context as _, Result, anyhow};
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
use cloud_llm_client::CompletionIntent; use cloud_llm_client::CompletionIntent;
use collections::HashSet; use collections::HashSet;
use gpui::{App, AppContext, AsyncApp, Entity, Task}; use gpui::{App, AppContext, AsyncApp, Entity, Task};
use indoc::formatdoc; use indoc::formatdoc;
use language::ToPoint;
use language::language_settings::{self, FormatOnSave}; use language::language_settings::{self, FormatOnSave};
use language_model::LanguageModelToolResultContent; use language_model::LanguageModelToolResultContent;
use paths; use paths;
@ -133,7 +134,7 @@ impl EditFileTool {
&self, &self,
input: &EditFileToolInput, input: &EditFileToolInput,
event_stream: &ToolCallEventStream, event_stream: &ToolCallEventStream,
cx: &App, cx: &mut App,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(())); return Task::ready(Ok(()));
@ -147,8 +148,9 @@ impl EditFileTool {
.components() .components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str()) .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{ {
return cx.foreground_executor().spawn( return event_stream.authorize(
event_stream.authorize(format!("{} (local settings)", input.display_description)), format!("{} (local settings)", input.display_description),
cx,
); );
} }
@ -156,9 +158,9 @@ impl EditFileTool {
// so check for that edge case too. // so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) { if canonical_path.starts_with(paths::config_dir()) {
return cx.foreground_executor().spawn( return event_stream.authorize(
event_stream format!("{} (global settings)", input.display_description),
.authorize(format!("{} (global settings)", input.display_description)), cx,
); );
} }
} }
@ -173,8 +175,7 @@ impl EditFileTool {
if project_path.is_some() { if project_path.is_some() {
Task::ready(Ok(())) Task::ready(Ok(()))
} else { } else {
cx.foreground_executor() event_stream.authorize(&input.display_description, cx)
.spawn(event_stream.authorize(input.display_description.clone()))
} }
} }
} }
@ -225,12 +226,22 @@ impl AgentTool for EditFileTool {
Ok(path) => path, Ok(path) => path,
Err(err) => return Task::ready(Err(anyhow!(err))), Err(err) => return Task::ready(Err(anyhow!(err))),
}; };
let abs_path = project.read(cx).absolute_path(&project_path, cx);
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: None,
}]),
..Default::default()
});
}
let request = self.thread.update(cx, |thread, cx| { let request = self.thread.update(cx, |thread, cx| {
thread.build_completion_request(CompletionIntent::ToolResults, cx) thread.build_completion_request(CompletionIntent::ToolResults, cx)
}); });
let thread = self.thread.read(cx); let thread = self.thread.read(cx);
let model = thread.selected_model.clone(); let model = thread.model().clone();
let action_log = thread.action_log().clone(); let action_log = thread.action_log().clone();
let authorize = self.authorize(&input, &event_stream, cx); let authorize = self.authorize(&input, &event_stream, cx);
@ -283,13 +294,38 @@ impl AgentTool for EditFileTool {
let mut hallucinated_old_text = false; let mut hallucinated_old_text = false;
let mut ambiguous_ranges = Vec::new(); let mut ambiguous_ranges = Vec::new();
let mut emitted_location = false;
while let Some(event) = events.next().await { while let Some(event) = events.next().await {
match event { match event {
EditAgentOutputEvent::Edited => {}, EditAgentOutputEvent::Edited(range) => {
if !emitted_location {
let line = buffer.update(cx, |buffer, _cx| {
range.start.to_point(&buffer.snapshot()).row
}).ok();
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
..Default::default()
});
}
emitted_location = true;
}
},
EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true,
EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
EditAgentOutputEvent::ResolvingEditRange(range) => { EditAgentOutputEvent::ResolvingEditRange(range) => {
diff.update(cx, |card, cx| card.reveal_range(range, cx))?; diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?;
// if !emitted_location {
// let line = buffer.update(cx, |buffer, _cx| {
// range.start.to_point(&buffer.snapshot()).row
// }).ok();
// if let Some(abs_path) = abs_path.clone() {
// event_stream.update_fields(ToolCallUpdateFields {
// locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
// ..Default::default()
// });
// }
// }
} }
} }
} }
@ -454,10 +490,9 @@ fn resolve_path(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::Templates;
use super::*; use super::*;
use assistant_tool::ActionLog; use crate::{ContextServerRegistry, Templates};
use action_log::ActionLog;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::Fs; use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal}; use gpui::{TestAppContext, UpdateGlobal};
@ -475,9 +510,20 @@ mod tests {
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = let thread = cx.new(|cx| {
cx.new(|_| Thread::new(project, Rc::default(), action_log, Templates::new(), model)); Thread::new(
project,
Rc::default(),
context_server_registry,
action_log,
Templates::new(),
model,
cx,
)
});
let result = cx let result = cx
.update(|cx| { .update(|cx| {
let input = EditFileToolInput { let input = EditFileToolInput {
@ -661,14 +707,18 @@ mod tests {
}); });
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
@ -792,15 +842,19 @@ mod tests {
.unwrap(); .unwrap();
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
@ -914,15 +968,19 @@ mod tests {
init_test(cx); init_test(cx);
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -942,8 +1000,11 @@ mod tests {
) )
}); });
let event = stream_rx.expect_tool_authorization().await; let event = stream_rx.expect_authorization().await;
assert_eq!(event.tool_call.title, "test 1 (local settings)"); assert_eq!(
event.tool_call.fields.title,
Some("test 1 (local settings)".into())
);
// Test 2: Path outside project should require confirmation // Test 2: Path outside project should require confirmation
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@ -959,8 +1020,8 @@ mod tests {
) )
}); });
let event = stream_rx.expect_tool_authorization().await; let event = stream_rx.expect_authorization().await;
assert_eq!(event.tool_call.title, "test 2"); assert_eq!(event.tool_call.fields.title, Some("test 2".into()));
// Test 3: Relative path without .zed should not require confirmation // Test 3: Relative path without .zed should not require confirmation
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@ -992,8 +1053,11 @@ mod tests {
cx, cx,
) )
}); });
let event = stream_rx.expect_tool_authorization().await; let event = stream_rx.expect_authorization().await;
assert_eq!(event.tool_call.title, "test 4 (local settings)"); assert_eq!(
event.tool_call.fields.title,
Some("test 4 (local settings)".into())
);
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed // Test 5: When always_allow_tool_actions is enabled, no confirmation needed
cx.update(|cx| { cx.update(|cx| {
@ -1041,15 +1105,19 @@ mod tests {
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await; fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project, project,
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1088,7 +1156,7 @@ mod tests {
}); });
if should_confirm { if should_confirm {
stream_rx.expect_tool_authorization().await; stream_rx.expect_authorization().await;
} else { } else {
auth.await.unwrap(); auth.await.unwrap();
assert!( assert!(
@ -1148,14 +1216,18 @@ mod tests {
.await; .await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry.clone(),
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1192,7 +1264,7 @@ mod tests {
}); });
if should_confirm { if should_confirm {
stream_rx.expect_tool_authorization().await; stream_rx.expect_authorization().await;
} else { } else {
auth.await.unwrap(); auth.await.unwrap();
assert!( assert!(
@ -1225,14 +1297,18 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry.clone(),
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1276,7 +1352,7 @@ mod tests {
}); });
if should_confirm { if should_confirm {
stream_rx.expect_tool_authorization().await; stream_rx.expect_authorization().await;
} else { } else {
auth.await.unwrap(); auth.await.unwrap();
assert!( assert!(
@ -1305,14 +1381,18 @@ mod tests {
.await; .await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry.clone(),
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });
@ -1339,7 +1419,7 @@ mod tests {
) )
}); });
stream_rx.expect_tool_authorization().await; stream_rx.expect_authorization().await;
// Test outside path with different modes // Test outside path with different modes
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@ -1355,7 +1435,7 @@ mod tests {
) )
}); });
stream_rx.expect_tool_authorization().await; stream_rx.expect_authorization().await;
// Test normal path with different modes // Test normal path with different modes
let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@ -1382,14 +1462,18 @@ mod tests {
let fs = project::FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default()); let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|_| { let thread = cx.new(|cx| {
Thread::new( Thread::new(
project.clone(), project.clone(),
Rc::default(), Rc::default(),
context_server_registry,
action_log.clone(), action_log.clone(),
Templates::new(), Templates::new(),
model.clone(), model.clone(),
cx,
) )
}); });
let tool = Arc::new(EditFileTool { thread }); let tool = Arc::new(EditFileTool { thread });

View file

@ -0,0 +1,155 @@
use std::rc::Rc;
use std::sync::Arc;
use std::{borrow::Cow, cell::RefCell};
use agent_client_protocol as acp;
use anyhow::{Context as _, Result, bail};
use futures::AsyncReadExt as _;
use gpui::{App, AppContext as _, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::SharedString;
use util::markdown::MarkdownEscaped;
use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
/// Fetches a URL and returns the content as Markdown.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FetchToolInput {
/// The URL to fetch.
url: String,
}
pub struct FetchTool {
http_client: Arc<HttpClientWithUrl>,
}
impl FetchTool {
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
Self { http_client }
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
Cow::Owned(format!("https://{url}"))
} else {
Cow::Borrowed(url)
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = if content_type.starts_with("text/plain") {
ContentType::Plaintext
} else if content_type.starts_with("application/json") {
ContentType::Json
} else {
ContentType::Html
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl AgentTool for FetchTool {
type Input = FetchToolInput;
type Output = String;
fn name(&self) -> SharedString {
"fetch".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
match input {
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
Err(_) => "Fetch URL".into(),
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let text = cx.background_spawn({
let http_client = self.http_client.clone();
async move { Self::build_message(http_client, &input.url).await }
});
cx.foreground_executor().spawn(async move {
let text = text.await?;
if text.trim().is_empty() {
bail!("no textual content found");
}
Ok(text)
})
}
}

View file

@ -1,6 +1,6 @@
use crate::{AgentTool, ToolCallEventStream}; use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use anyhow::{anyhow, Result}; use anyhow::{Result, anyhow};
use gpui::{App, AppContext, Entity, SharedString, Task}; use gpui::{App, AppContext, Entity, SharedString, Task};
use language_model::LanguageModelToolResultContent; use language_model::LanguageModelToolResultContent;
use project::Project; use project::Project;
@ -139,9 +139,6 @@ impl AgentTool for FindPathTool {
}) })
.collect(), .collect(),
), ),
raw_output: Some(serde_json::json!({
"paths": &matches,
})),
..Default::default() ..Default::default()
}); });

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,664 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Result, anyhow};
use gpui::{App, Entity, SharedString, Task};
use project::{Project, WorktreeSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::fmt::Write;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
/// Lists files and directories in a given path. Prefer the `grep` or
/// `find_path` tools when searching the codebase.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ListDirectoryToolInput {
/// The fully-qualified path of the directory to list in the project.
///
/// This path should never be absolute, and the first component
/// of the path should always be a root directory in a project.
///
/// <example>
/// If the project has the following root directories:
///
/// - directory1
/// - directory2
///
/// You can list the contents of `directory1` by using the path `directory1`.
/// </example>
///
/// <example>
/// If the project has the following root directories:
///
/// - foo
/// - bar
///
/// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`.
/// </example>
pub path: String,
}
pub struct ListDirectoryTool {
project: Entity<Project>,
}
impl ListDirectoryTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for ListDirectoryTool {
type Input = ListDirectoryToolInput;
type Output = String;
fn name(&self) -> SharedString {
"list_directory".into()
}
fn kind(&self) -> ToolKind {
ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
let path = MarkdownInlineCode(&input.path);
format!("List the {path} directory's contents").into()
} else {
"List directory".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
// Sometimes models will return these even though we tell it to give a path and not a glob.
// When this happens, just list the root worktree directories.
if matches!(input.path.as_str(), "." | "" | "./" | "*") {
let output = self
.project
.read(cx)
.worktrees(cx)
.filter_map(|worktree| {
worktree.read(cx).root_entry().and_then(|entry| {
if entry.is_dir() {
entry.path.to_str()
} else {
None
}
})
})
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(output));
}
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", input.path)));
};
let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found")));
};
// Check if the directory whose contents we're listing is itself excluded or private
let global_settings = WorktreeSettings::get_global(cx);
if global_settings.is_path_excluded(&project_path.path) {
return Task::ready(Err(anyhow!(
"Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}",
&input.path
)));
}
if global_settings.is_path_private(&project_path.path) {
return Task::ready(Err(anyhow!(
"Cannot list directory because its path matches the user's global `private_files` setting: {}",
&input.path
)));
}
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
if worktree_settings.is_path_excluded(&project_path.path) {
return Task::ready(Err(anyhow!(
"Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}",
&input.path
)));
}
if worktree_settings.is_path_private(&project_path.path) {
return Task::ready(Err(anyhow!(
"Cannot list directory because its path matches the user's worktree `private_paths` setting: {}",
&input.path
)));
}
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_root_name = worktree.read(cx).root_name().to_string();
let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else {
return Task::ready(Err(anyhow!("Path not found: {}", input.path)));
};
if !entry.is_dir() {
return Task::ready(Err(anyhow!("{} is not a directory.", input.path)));
}
let worktree_snapshot = worktree.read(cx).snapshot();
let mut folders = Vec::new();
let mut files = Vec::new();
for entry in worktree_snapshot.child_entries(&project_path.path) {
// Skip private and excluded files and directories
if global_settings.is_path_private(&entry.path)
|| global_settings.is_path_excluded(&entry.path)
{
continue;
}
if self
.project
.read(cx)
.find_project_path(&entry.path, cx)
.map(|project_path| {
let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx);
worktree_settings.is_path_excluded(&project_path.path)
|| worktree_settings.is_path_private(&project_path.path)
})
.unwrap_or(false)
{
continue;
}
let full_path = Path::new(&worktree_root_name)
.join(&entry.path)
.display()
.to_string();
if entry.is_dir() {
folders.push(full_path);
} else {
files.push(full_path);
}
}
let mut output = String::new();
if !folders.is_empty() {
writeln!(output, "# Folders:\n{}", folders.join("\n")).unwrap();
}
if !files.is_empty() {
writeln!(output, "\n# Files:\n{}", files.join("\n")).unwrap();
}
if output.is_empty() {
writeln!(output, "{} is empty.", input.path).unwrap();
}
Task::ready(Ok(output))
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{TestAppContext, UpdateGlobal};
use indoc::indoc;
use project::{FakeFs, Project, WorktreeSettings};
use serde_json::json;
use settings::SettingsStore;
use util::path;
fn platform_paths(path_str: &str) -> String {
if cfg!(target_os = "windows") {
path_str.replace("/", "\\")
} else {
path_str.to_string()
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
#[gpui::test]
async fn test_list_directory_separates_files_and_dirs(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
"src": {
"main.rs": "fn main() {}",
"lib.rs": "pub fn hello() {}",
"models": {
"user.rs": "struct User {}",
"post.rs": "struct Post {}"
},
"utils": {
"helper.rs": "pub fn help() {}"
}
},
"tests": {
"integration_test.rs": "#[test] fn test() {}"
},
"README.md": "# Project",
"Cargo.toml": "[package]"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let tool = Arc::new(ListDirectoryTool::new(project));
// Test listing root directory
let input = ListDirectoryToolInput {
path: "project".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert_eq!(
output,
platform_paths(indoc! {"
# Folders:
project/src
project/tests
# Files:
project/Cargo.toml
project/README.md
"})
);
// Test listing src directory
let input = ListDirectoryToolInput {
path: "project/src".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert_eq!(
output,
platform_paths(indoc! {"
# Folders:
project/src/models
project/src/utils
# Files:
project/src/lib.rs
project/src/main.rs
"})
);
// Test listing directory with only files
let input = ListDirectoryToolInput {
path: "project/tests".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert!(!output.contains("# Folders:"));
assert!(output.contains("# Files:"));
assert!(output.contains(&platform_paths("project/tests/integration_test.rs")));
}
#[gpui::test]
async fn test_list_directory_empty_directory(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
"empty_dir": {}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let tool = Arc::new(ListDirectoryTool::new(project));
let input = ListDirectoryToolInput {
path: "project/empty_dir".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert_eq!(output, "project/empty_dir is empty.\n");
}
#[gpui::test]
async fn test_list_directory_error_cases(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
"file.txt": "content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let tool = Arc::new(ListDirectoryTool::new(project));
// Test non-existent path
let input = ListDirectoryToolInput {
path: "project/nonexistent".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await;
assert!(output.unwrap_err().to_string().contains("Path not found"));
// Test trying to list a file instead of directory
let input = ListDirectoryToolInput {
path: "project/file.txt".into(),
};
let output = cx
.update(|cx| tool.run(input, ToolCallEventStream::test().0, cx))
.await;
assert!(
output
.unwrap_err()
.to_string()
.contains("is not a directory")
);
}
#[gpui::test]
async fn test_list_directory_security(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/project"),
json!({
"normal_dir": {
"file1.txt": "content",
"file2.txt": "content"
},
".mysecrets": "SECRET_KEY=abc123",
".secretdir": {
"config": "special configuration",
"secret.txt": "secret content"
},
".mymetadata": "custom metadata",
"visible_dir": {
"normal.txt": "normal content",
"special.privatekey": "private key content",
"data.mysensitive": "sensitive data",
".hidden_subdir": {
"hidden_file.txt": "hidden content"
}
}
}),
)
.await;
// Configure settings explicitly
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions = Some(vec![
"**/.secretdir".to_string(),
"**/.mymetadata".to_string(),
"**/.hidden_subdir".to_string(),
]);
settings.private_files = Some(vec![
"**/.mysecrets".to_string(),
"**/*.privatekey".to_string(),
"**/*.mysensitive".to_string(),
]);
});
});
});
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
let tool = Arc::new(ListDirectoryTool::new(project));
// Listing root directory should exclude private and excluded files
let input = ListDirectoryToolInput {
path: "project".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
// Should include normal directories
assert!(output.contains("normal_dir"), "Should list normal_dir");
assert!(output.contains("visible_dir"), "Should list visible_dir");
// Should NOT include excluded or private files
assert!(
!output.contains(".secretdir"),
"Should not list .secretdir (file_scan_exclusions)"
);
assert!(
!output.contains(".mymetadata"),
"Should not list .mymetadata (file_scan_exclusions)"
);
assert!(
!output.contains(".mysecrets"),
"Should not list .mysecrets (private_files)"
);
// Trying to list an excluded directory should fail
let input = ListDirectoryToolInput {
path: "project/.secretdir".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await;
assert!(
output
.unwrap_err()
.to_string()
.contains("file_scan_exclusions"),
"Error should mention file_scan_exclusions"
);
// Listing a directory should exclude private files within it
let input = ListDirectoryToolInput {
path: "project/visible_dir".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
// Should include normal files
assert!(output.contains("normal.txt"), "Should list normal.txt");
// Should NOT include private files
assert!(
!output.contains("privatekey"),
"Should not list .privatekey files (private_files)"
);
assert!(
!output.contains("mysensitive"),
"Should not list .mysensitive files (private_files)"
);
// Should NOT include subdirectories that match exclusions
assert!(
!output.contains(".hidden_subdir"),
"Should not list .hidden_subdir (file_scan_exclusions)"
);
}
#[gpui::test]
async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
// Create first worktree with its own private files
fs.insert_tree(
path!("/worktree1"),
json!({
".zed": {
"settings.json": r#"{
"file_scan_exclusions": ["**/fixture.*"],
"private_files": ["**/secret.rs", "**/config.toml"]
}"#
},
"src": {
"main.rs": "fn main() { println!(\"Hello from worktree1\"); }",
"secret.rs": "const API_KEY: &str = \"secret_key_1\";",
"config.toml": "[database]\nurl = \"postgres://localhost/db1\""
},
"tests": {
"test.rs": "mod tests { fn test_it() {} }",
"fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));"
}
}),
)
.await;
// Create second worktree with different private files
fs.insert_tree(
path!("/worktree2"),
json!({
".zed": {
"settings.json": r#"{
"file_scan_exclusions": ["**/internal.*"],
"private_files": ["**/private.js", "**/data.json"]
}"#
},
"lib": {
"public.js": "export function greet() { return 'Hello from worktree2'; }",
"private.js": "const SECRET_TOKEN = \"private_token_2\";",
"data.json": "{\"api_key\": \"json_secret_key\"}"
},
"docs": {
"README.md": "# Public Documentation",
"internal.md": "# Internal Secrets and Configuration"
}
}),
)
.await;
// Set global settings
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<WorktreeSettings>(cx, |settings| {
settings.file_scan_exclusions =
Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]);
settings.private_files = Some(vec!["**/.env".to_string()]);
});
});
});
let project = Project::test(
fs.clone(),
[path!("/worktree1").as_ref(), path!("/worktree2").as_ref()],
cx,
)
.await;
// Wait for worktrees to be fully scanned
cx.executor().run_until_parked();
let tool = Arc::new(ListDirectoryTool::new(project));
// Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings
let input = ListDirectoryToolInput {
path: "worktree1/src".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert!(output.contains("main.rs"), "Should list main.rs");
assert!(
!output.contains("secret.rs"),
"Should not list secret.rs (local private_files)"
);
assert!(
!output.contains("config.toml"),
"Should not list config.toml (local private_files)"
);
// Test listing worktree1/tests - should exclude fixture.sql based on local settings
let input = ListDirectoryToolInput {
path: "worktree1/tests".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert!(output.contains("test.rs"), "Should list test.rs");
assert!(
!output.contains("fixture.sql"),
"Should not list fixture.sql (local file_scan_exclusions)"
);
// Test listing worktree2/lib - should exclude private.js and data.json based on local settings
let input = ListDirectoryToolInput {
path: "worktree2/lib".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert!(output.contains("public.js"), "Should list public.js");
assert!(
!output.contains("private.js"),
"Should not list private.js (local private_files)"
);
assert!(
!output.contains("data.json"),
"Should not list data.json (local private_files)"
);
// Test listing worktree2/docs - should exclude internal.md based on local settings
let input = ListDirectoryToolInput {
path: "worktree2/docs".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await
.unwrap();
assert!(output.contains("README.md"), "Should list README.md");
assert!(
!output.contains("internal.md"),
"Should not list internal.md (local file_scan_exclusions)"
);
// Test trying to list an excluded directory directly
let input = ListDirectoryToolInput {
path: "worktree1/src/secret.rs".into(),
};
let output = cx
.update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx))
.await;
assert!(
output
.unwrap_err()
.to_string()
.contains("Cannot list directory"),
);
}
}

View file

@ -0,0 +1,123 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownInlineCode;
/// Moves or rename a file or directory in the project, and returns confirmation
/// that the move succeeded.
///
/// If the source and destination directories are the same, but the filename is
/// different, this performs a rename. Otherwise, it performs a move.
///
/// This tool should be used when it's desirable to move or rename a file or
/// directory without changing its contents at all.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct MovePathToolInput {
/// The source path of the file or directory to move/rename.
///
/// <example>
/// If the project has the following files:
///
/// - directory1/a/something.txt
/// - directory2/a/things.txt
/// - directory3/a/other.txt
///
/// You can move the first file by providing a source_path of "directory1/a/something.txt"
/// </example>
pub source_path: String,
/// The destination path where the file or directory should be moved/renamed to.
/// If the paths are the same except for the filename, then this will be a rename.
///
/// <example>
/// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
/// provide a destination_path of "directory2/b/renamed.txt"
/// </example>
pub destination_path: String,
}
pub struct MovePathTool {
project: Entity<Project>,
}
impl MovePathTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for MovePathTool {
type Input = MovePathToolInput;
type Output = String;
fn name(&self) -> SharedString {
"move_path".into()
}
fn kind(&self) -> ToolKind {
ToolKind::Move
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);
let src_path = Path::new(&input.source_path);
let dest_path = Path::new(&input.destination_path);
match dest_path
.file_name()
.and_then(|os_str| os_str.to_os_string().into_string().ok())
{
Some(filename) if src_path.parent() == dest_path.parent() => {
let filename = MarkdownInlineCode(&filename);
format!("Rename {src} to {filename}").into()
}
_ => format!("Move {src} to {dest}").into(),
}
} else {
"Move path".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let rename_task = self.project.update(cx, |project, cx| {
match project
.find_project_path(&input.source_path, cx)
.and_then(|project_path| project.entry_for_path(&project_path, cx))
{
Some(entity) => match project.find_project_path(&input.destination_path, cx) {
Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
None => Task::ready(Err(anyhow!(
"Destination path {} was outside the project.",
input.destination_path
))),
},
None => Task::ready(Err(anyhow!(
"Source path {} was not found in the project.",
input.source_path
))),
}
});
cx.background_spawn(async move {
let _ = rename_task.await.with_context(|| {
format!("Moving {} to {}", input.source_path, input.destination_path)
})?;
Ok(format!(
"Moved {} to {}",
input.source_path, input.destination_path
))
})
}
}

View file

@ -0,0 +1,59 @@
use std::sync::Arc;
use agent_client_protocol as acp;
use anyhow::Result;
use chrono::{Local, Utc};
use gpui::{App, SharedString, Task};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Timezone {
/// Use UTC for the datetime.
Utc,
/// Use local time for the datetime.
Local,
}
/// Returns the current datetime in RFC 3339 format.
/// Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct NowToolInput {
/// The timezone to use for the datetime.
timezone: Timezone,
}
pub struct NowTool;
impl AgentTool for NowTool {
type Input = NowToolInput;
type Output = String;
fn name(&self) -> SharedString {
"now".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Other
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Get current time".into()
}
fn run(
self: Arc<Self>,
input: Self::Input,
_event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Task<Result<String>> {
let now = match input.timezone {
Timezone::Utc => Utc::now().to_rfc3339(),
Timezone::Local => Local::now().to_rfc3339(),
};
Task::ready(Ok(format!("The current datetime is {now}.")))
}
}

View file

@ -0,0 +1,170 @@
use crate::AgentTool;
use agent_client_protocol::ToolKind;
use anyhow::{Context as _, Result};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use util::markdown::MarkdownEscaped;
/// This tool opens a file or URL with the default application associated with
/// it on the user's operating system:
///
/// - On macOS, it's equivalent to the `open` command
/// - On Windows, it's equivalent to `start`
/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
///
/// For example, it can open a web browser with a URL, open a PDF file with the
/// default PDF viewer, etc.
///
/// You MUST ONLY use this tool when the user has explicitly requested opening
/// something. You MUST NEVER assume that the user would like for you to use
/// this tool.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct OpenToolInput {
/// The path or URL to open with the default application.
path_or_url: String,
}
pub struct OpenTool {
project: Entity<Project>,
}
impl OpenTool {
pub fn new(project: Entity<Project>) -> Self {
Self { project }
}
}
impl AgentTool for OpenTool {
type Input = OpenToolInput;
type Output = String;
fn name(&self) -> SharedString {
"open".into()
}
fn kind(&self) -> ToolKind {
ToolKind::Execute
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
} else {
"Open file or URL".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
event_stream: crate::ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
// If path_or_url turns out to be a path in the project, make it absolute.
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.background_spawn(async move {
authorize.await?;
match abs_path {
Some(path) => open::that(path),
None => open::that(&input.path_or_url),
}
.context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url))
})
}
}
fn to_absolute_path(
potential_path: &str,
project: Entity<Project>,
cx: &mut App,
) -> Option<PathBuf> {
let project = project.read(cx);
project
.find_project_path(PathBuf::from(potential_path), cx)
.and_then(|project_path| project.absolute_path(&project_path, cx))
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use project::{FakeFs, Project};
use settings::SettingsStore;
use std::path::Path;
use tempfile::TempDir;
#[gpui::test]
async fn test_to_absolute_path(cx: &mut TestAppContext) {
init_test(cx);
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let temp_path = temp_dir.path().to_string_lossy().to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
&temp_path,
serde_json::json!({
"src": {
"main.rs": "fn main() {}",
"lib.rs": "pub fn lib_fn() {}"
},
"docs": {
"readme.md": "# Project Documentation"
}
}),
)
.await;
// Use the temp_path as the root directory, not just its filename
let project = Project::test(fs.clone(), [temp_dir.path()], cx).await;
// Test cases where the function should return Some
cx.update(|cx| {
// Project-relative paths should return Some
// Create paths using the last segment of the temp path to simulate a project-relative path
let root_dir_name = Path::new(&temp_path)
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new("temp"))
.to_string_lossy();
assert!(
to_absolute_path(&format!("{root_dir_name}/src/main.rs"), project.clone(), cx)
.is_some(),
"Failed to resolve main.rs path"
);
assert!(
to_absolute_path(
&format!("{root_dir_name}/docs/readme.md",),
project.clone(),
cx,
)
.is_some(),
"Failed to resolve readme.md path"
);
// External URL should return None
let result = to_absolute_path("https://example.com", project.clone(), cx);
assert_eq!(result, None, "External URLs should return None");
// Path outside project
let result = to_absolute_path("../invalid/path", project.clone(), cx);
assert_eq!(result, None, "Paths outside the project should return None");
});
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View file

@ -1,16 +1,16 @@
use agent_client_protocol::{self as acp}; use action_log::ActionLog;
use anyhow::{anyhow, Context, Result}; use agent_client_protocol::{self as acp, ToolCallUpdateFields};
use assistant_tool::{outline, ActionLog}; use anyhow::{Context as _, Result, anyhow};
use gpui::{Entity, Task}; use assistant_tool::outline;
use gpui::{App, Entity, SharedString, Task};
use indoc::formatdoc; use indoc::formatdoc;
use language::{Anchor, Point}; use language::Point;
use language_model::{LanguageModelImage, LanguageModelToolResultContent}; use language_model::{LanguageModelImage, LanguageModelToolResultContent};
use project::{image_store, AgentLocation, ImageItem, Project, WorktreeSettings}; use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use ui::{App, SharedString};
use crate::{AgentTool, ToolCallEventStream}; use crate::{AgentTool, ToolCallEventStream};
@ -97,7 +97,7 @@ impl AgentTool for ReadFileTool {
fn run( fn run(
self: Arc<Self>, self: Arc<Self>,
input: Self::Input, input: Self::Input,
_event_stream: ToolCallEventStream, event_stream: ToolCallEventStream,
cx: &mut App, cx: &mut App,
) -> Task<Result<LanguageModelToolResultContent>> { ) -> Task<Result<LanguageModelToolResultContent>> {
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
@ -166,7 +166,9 @@ impl AgentTool for ReadFileTool {
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
let buffer = cx let buffer = cx
.update(|cx| { .update(|cx| {
project.update(cx, |project, cx| project.open_buffer(project_path, cx)) project.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
})? })?
.await?; .await?;
if buffer.read_with(cx, |buffer, _| { if buffer.read_with(cx, |buffer, _| {
@ -178,19 +180,10 @@ impl AgentTool for ReadFileTool {
anyhow::bail!("{file_path} not found"); anyhow::bail!("{file_path} not found");
} }
project.update(cx, |project, cx| { let mut anchor = None;
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: Anchor::MIN,
}),
cx,
);
})?;
// Check if specific line ranges are provided // Check if specific line ranges are provided
if input.start_line.is_some() || input.end_line.is_some() { let result = if input.start_line.is_some() || input.end_line.is_some() {
let mut anchor = None;
let result = buffer.read_with(cx, |buffer, _cx| { let result = buffer.read_with(cx, |buffer, _cx| {
let text = buffer.text(); let text = buffer.text();
// .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
@ -214,18 +207,6 @@ impl AgentTool for ReadFileTool {
log.buffer_read(buffer.clone(), cx); log.buffer_read(buffer.clone(), cx);
})?; })?;
if let Some(anchor) = anchor {
project.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor,
}),
cx,
);
})?;
}
Ok(result.into()) Ok(result.into())
} else { } else {
// No line ranges specified, so check file size to see if it's too big. // No line ranges specified, so check file size to see if it's too big.
@ -236,7 +217,7 @@ impl AgentTool for ReadFileTool {
let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?; let result = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
action_log.update(cx, |log, cx| { action_log.update(cx, |log, cx| {
log.buffer_read(buffer, cx); log.buffer_read(buffer.clone(), cx);
})?; })?;
Ok(result.into()) Ok(result.into())
@ -244,7 +225,8 @@ impl AgentTool for ReadFileTool {
// File is too big, so return the outline // File is too big, so return the outline
// and a suggestion to read again with line numbers. // and a suggestion to read again with line numbers.
let outline = let outline =
outline::file_outline(project, file_path, action_log, None, cx).await?; outline::file_outline(project.clone(), file_path, action_log, None, cx)
.await?;
Ok(formatdoc! {" Ok(formatdoc! {"
This file was too big to read all at once. This file was too big to read all at once.
@ -261,7 +243,28 @@ impl AgentTool for ReadFileTool {
} }
.into()) .into())
} }
} };
project.update(cx, |project, cx| {
if let Some(abs_path) = project.absolute_path(&project_path, cx) {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor.unwrap_or(text::Anchor::MIN),
}),
cx,
);
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: input.start_line.map(|line| line.saturating_sub(1)),
}]),
..Default::default()
});
}
})?;
result
}) })
} }
} }
@ -270,7 +273,7 @@ impl AgentTool for ReadFileTool {
mod test { mod test {
use super::*; use super::*;
use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher}; use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;

View file

@ -0,0 +1,473 @@
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 schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
use crate::{AgentTool, ToolCallEventStream};
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
/// Executes a shell one-liner and returns the combined output.
///
/// This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result.
///
/// The output results will be shown to the user already, only list it again if necessary, avoid being redundant.
///
/// Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error.
///
/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own.
///
/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct TerminalToolInput {
/// The one-liner command to execute.
command: String,
/// Working directory for the command. This must be one of the root directories of the project.
cd: String,
}
pub struct TerminalTool {
project: Entity<Project>,
determine_shell: Shared<Task<String>>,
}
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() {
log::info!("agent selected bash for terminal tool");
"bash".into()
} else {
let shell = get_system_shell();
log::info!("agent selected {shell} for terminal tool");
shell
}
});
Self {
project,
determine_shell: determine_shell.shared(),
}
}
}
impl AgentTool for TerminalTool {
type Input = TerminalToolInput;
type Output = String;
fn name(&self) -> SharedString {
"terminal".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Execute
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
let mut lines = input.command.lines();
let first_line = lines.next().unwrap_or_default();
let remaining_line_count = lines.count();
match remaining_line_count {
0 => MarkdownInlineCode(&first_line).to_string().into(),
1 => MarkdownInlineCode(&format!(
"{} - {} more line",
first_line, remaining_line_count
))
.to_string()
.into(),
n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n))
.to_string()
.into(),
}
} else {
"Run terminal command".into()
}
}
fn run(
self: Arc<Self>,
input: Self::Input,
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?;
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 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 (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)
}
})
}
}
fn process_content(
content: &str,
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();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let content = if should_truncate {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
content.len(),
)
} else {
content
};
let content = match exit_status {
Some(exit_status) if exit_status.success() => {
if is_empty {
"Command executed successfully.".to_string()
} else {
content.to_string()
}
}
Some(exit_status) => {
if is_empty {
format!(
"Command \"{command}\" failed with exit code {}.",
exit_status.exit_code()
)
} else {
format!(
"Command \"{command}\" failed with exit code {}.\n\n{content}",
exit_status.exit_code()
)
}
}
None => {
format!(
"Command failed or was interrupted.\nPartial output captured:\n\n{}",
content,
)
}
};
(content, is_empty)
}
fn working_dir(
input: &TerminalToolInput,
project: &Entity<Project>,
cx: &mut App,
) -> Result<Option<PathBuf>> {
let project = project.read(cx);
let cd = &input.cd;
if cd == "." || cd == "" {
// Accept "." or "" as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
match worktrees.next() {
Some(worktree) => {
anyhow::ensure!(
worktrees.next().is_none(),
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
Ok(Some(worktree.read(cx).abs_path().to_path_buf()))
}
None => Ok(None),
}
} else {
let input_path = Path::new(cd);
if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Ok(Some(input_path.into()));
}
} else {
if let Some(worktree) = project.worktree_for_root_name(cd, cx) {
return Ok(Some(worktree.read(cx).abs_path().to_path_buf()));
}
}
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::AgentResponseEvent;
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(AgentResponseEvent::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;
}
}

View file

@ -0,0 +1,112 @@
use std::sync::Arc;
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use cloud_llm_client::WebSearchResponse;
use gpui::{App, AppContext, Task};
use language_model::{
LanguageModelProviderId, LanguageModelToolResultContent, ZED_CLOUD_PROVIDER_ID,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use ui::prelude::*;
use web_search::WebSearchRegistry;
/// Search the web for information using your query.
/// Use this when you need real-time information, facts, or data that might not be in your training. \
/// Results will include snippets and links from relevant web pages.
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
/// The search term or question to query on the web.
query: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WebSearchToolOutput(WebSearchResponse);
impl From<WebSearchToolOutput> for LanguageModelToolResultContent {
fn from(value: WebSearchToolOutput) -> Self {
serde_json::to_string(&value.0)
.expect("Failed to serialize WebSearchResponse")
.into()
}
}
pub struct WebSearchTool;
impl AgentTool for WebSearchTool {
type Input = WebSearchToolInput;
type Output = WebSearchToolOutput;
fn name(&self) -> SharedString {
"web_search".into()
}
fn kind(&self) -> acp::ToolKind {
acp::ToolKind::Fetch
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Searching the Web".into()
}
/// We currently only support Zed Cloud as a provider.
fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool {
provider == &ZED_CLOUD_PROVIDER_ID
}
fn run(
self: Arc<Self>,
input: Self::Input,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else {
return Task::ready(Err(anyhow!("Web search is not available.")));
};
let search_task = provider.search(input.query, cx);
cx.background_spawn(async move {
let response = match search_task.await {
Ok(response) => response,
Err(err) => {
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some("Web Search Failed".to_string()),
..Default::default()
});
return Err(err);
}
};
let result_text = if response.results.len() == 1 {
"1 result".to_string()
} else {
format!("{} results", response.results.len())
};
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(format!("Searched the web: {result_text}")),
content: Some(
response
.results
.iter()
.map(|result| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: result.title.clone(),
uri: result.url.clone(),
title: Some(result.title.clone()),
description: Some(result.text.clone()),
mime_type: None,
annotations: None,
size: None,
}),
})
.collect(),
),
..Default::default()
});
Ok(WebSearchToolOutput(response))
})
}
}

View file

@ -5,7 +5,7 @@ use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project; use project::Project;
use std::{cell::RefCell, path::Path, rc::Rc}; use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
use ui::App; use ui::App;
use util::ResultExt as _; use util::ResultExt as _;
@ -135,9 +135,9 @@ impl acp_old::Client for OldAcpClientDelegate {
let response = cx let response = cx
.update(|cx| { .update(|cx| {
self.thread.borrow().update(cx, |thread, cx| { self.thread.borrow().update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call, acp_options, cx) thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
}) })
})? })??
.context("Failed to update thread")? .context("Failed to update thread")?
.await; .await;
@ -168,7 +168,7 @@ impl acp_old::Client for OldAcpClientDelegate {
cx, cx,
) )
}) })
})? })??
.context("Failed to update thread")?; .context("Failed to update thread")?;
Ok(acp_old::PushToolCallResponse { Ok(acp_old::PushToolCallResponse {
@ -423,7 +423,7 @@ impl AgentConnection for AcpConnection {
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
_cwd: &Path, _cwd: &Path,
cx: &mut AsyncApp, cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> { ) -> Task<Result<Entity<AcpThread>>> {
let task = self.connection.request_any( let task = self.connection.request_any(
acp_old::InitializeParams { acp_old::InitializeParams {
@ -467,6 +467,7 @@ impl AgentConnection for AcpConnection {
fn prompt( fn prompt(
&self, &self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest, params: acp::PromptRequest,
cx: &mut App, cx: &mut App,
) -> Task<Result<acp::PromptResponse>> { ) -> Task<Result<acp::PromptResponse>> {
@ -506,4 +507,8 @@ impl AgentConnection for AcpConnection {
}) })
.detach_and_log_err(cx) .detach_and_log_err(cx)
} }
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
} }

View file

@ -1,11 +1,13 @@
use agent_client_protocol::{self as acp, Agent as _}; use agent_client_protocol::{self as acp, Agent as _};
use anyhow::anyhow; use anyhow::anyhow;
use collections::HashMap; use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot; use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project; use project::Project;
use std::cell::RefCell;
use std::path::Path; use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use std::{any::Any, cell::RefCell};
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
@ -40,12 +42,13 @@ impl AcpConnection {
.current_dir(root_dir) .current_dir(root_dir)
.stdin(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit()) .stderr(std::process::Stdio::piped())
.kill_on_drop(true) .kill_on_drop(true)
.spawn()?; .spawn()?;
let stdout = child.stdout.take().expect("Failed to take stdout"); let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().expect("Failed to take stdin"); let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::trace!("Spawned (pid: {})", child.id()); log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default())); let sessions = Rc::new(RefCell::new(HashMap::default()));
@ -63,6 +66,18 @@ impl AcpConnection {
let io_task = cx.background_spawn(io_task); let io_task = cx.background_spawn(io_task);
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
cx.spawn({ cx.spawn({
let sessions = sessions.clone(); let sessions = sessions.clone();
async move |cx| { async move |cx| {
@ -111,7 +126,7 @@ impl AgentConnection for AcpConnection {
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
cwd: &Path, cwd: &Path,
cx: &mut AsyncApp, cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> { ) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone(); let conn = self.connection.clone();
let sessions = self.sessions.clone(); let sessions = self.sessions.clone();
@ -171,6 +186,7 @@ impl AgentConnection for AcpConnection {
fn prompt( fn prompt(
&self, &self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest, params: acp::PromptRequest,
cx: &mut App, cx: &mut App,
) -> Task<Result<acp::PromptResponse>> { ) -> Task<Result<acp::PromptResponse>> {
@ -190,6 +206,10 @@ impl AgentConnection for AcpConnection {
.spawn(async move { conn.cancel(params).await }) .spawn(async move { conn.cancel(params).await })
.detach(); .detach();
} }
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
} }
struct ClientDelegate { struct ClientDelegate {
@ -213,7 +233,7 @@ impl acp::Client for ClientDelegate {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?; })?;
let result = rx.await; let result = rx?.await;
let outcome = match result { let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },

View file

@ -6,6 +6,7 @@ use context_server::listener::McpServerTool;
use project::Project; use project::Project;
use settings::SettingsStore; use settings::SettingsStore;
use smol::process::Child; use smol::process::Child;
use std::any::Any;
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Display; use std::fmt::Display;
use std::path::Path; use std::path::Path;
@ -13,7 +14,7 @@ use std::rc::Rc;
use uuid::Uuid; use uuid::Uuid;
use agent_client_protocol as acp; use agent_client_protocol as acp;
use anyhow::{Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot; use futures::channel::oneshot;
use futures::{AsyncBufReadExt, AsyncWriteExt}; use futures::{AsyncBufReadExt, AsyncWriteExt};
use futures::{ use futures::{
@ -74,7 +75,7 @@ impl AgentConnection for ClaudeAgentConnection {
self: Rc<Self>, self: Rc<Self>,
project: Entity<Project>, project: Entity<Project>,
cwd: &Path, cwd: &Path,
cx: &mut AsyncApp, cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> { ) -> Task<Result<Entity<AcpThread>>> {
let cwd = cwd.to_owned(); let cwd = cwd.to_owned();
cx.spawn(async move |cx| { cx.spawn(async move |cx| {
@ -129,12 +130,25 @@ impl AgentConnection for ClaudeAgentConnection {
&cwd, &cwd,
)?; )?;
let stdin = child.stdin.take().unwrap(); let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdout = child.stdout.take().unwrap(); let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
let pid = child.id(); let pid = child.id();
log::trace!("Spawned (pid: {})", pid); log::trace!("Spawned (pid: {})", pid);
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
cx.background_spawn(async move { cx.background_spawn(async move {
let mut outgoing_rx = Some(outgoing_rx); let mut outgoing_rx = Some(outgoing_rx);
@ -210,6 +224,7 @@ impl AgentConnection for ClaudeAgentConnection {
fn prompt( fn prompt(
&self, &self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest, params: acp::PromptRequest,
cx: &mut App, cx: &mut App,
) -> Task<Result<acp::PromptResponse>> { ) -> Task<Result<acp::PromptResponse>> {
@ -288,6 +303,10 @@ impl AgentConnection for ClaudeAgentConnection {
}) })
.log_err(); .log_err();
} }
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -339,7 +358,7 @@ fn spawn_claude(
.current_dir(root_dir) .current_dir(root_dir)
.stdin(std::process::Stdio::piped()) .stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit()) .stderr(std::process::Stdio::piped())
.kill_on_drop(true) .kill_on_drop(true)
.spawn()?; .spawn()?;
@ -423,7 +442,7 @@ impl ClaudeAgentSession {
if !turn_state.borrow().is_cancelled() { if !turn_state.borrow().is_cancelled() {
thread thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.push_user_content_block(text.into(), cx) thread.push_user_content_block(None, text.into(), cx)
}) })
.log_err(); .log_err();
} }
@ -541,8 +560,9 @@ impl ClaudeAgentSession {
thread.upsert_tool_call( thread.upsert_tool_call(
claude_tool.as_acp(acp::ToolCallId(id.into())), claude_tool.as_acp(acp::ToolCallId(id.into())),
cx, cx,
); )?;
} }
anyhow::Ok(())
}) })
.log_err(); .log_err();
} }

View file

@ -154,7 +154,7 @@ impl McpServerTool for PermissionTool {
let chosen_option = thread let chosen_option = thread
.update(cx, |thread, cx| { .update(cx, |thread, cx| {
thread.request_tool_call_authorization( thread.request_tool_call_authorization(
claude_tool.as_acp(tool_call_id), claude_tool.as_acp(tool_call_id).into(),
vec![ vec![
acp::PermissionOption { acp::PermissionOption {
id: allow_option_id.clone(), id: allow_option_id.clone(),
@ -169,7 +169,7 @@ impl McpServerTool for PermissionTool {
], ],
cx, cx,
) )
})? })??
.await?; .await?;
let response = if chosen_option == allow_option_id { let response = if chosen_option == allow_option_id {

View file

@ -422,8 +422,8 @@ pub async fn new_test_thread(
.await .await
.unwrap(); .unwrap();
let thread = connection let thread = cx
.new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async()) .update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx))
.await .await
.unwrap(); .unwrap();

View file

@ -48,6 +48,20 @@ pub struct AgentProfileSettings {
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
} }
impl AgentProfileSettings {
pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
self.tools.get(tool_name) == Some(&true)
}
pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool {
self.enable_all_context_servers
|| self
.context_servers
.get(server_id)
.map_or(false, |preset| preset.tools.get(tool_name) == Some(&true))
}
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ContextServerPreset { pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>, pub tools: IndexMap<Arc<str>, bool>,

View file

@ -309,7 +309,7 @@ pub struct AgentSettingsContent {
/// ///
/// Default: true /// Default: true
expand_terminal_card: Option<bool>, expand_terminal_card: Option<bool>,
/// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel. /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
/// ///
/// Default: false /// Default: false
use_modifier_to_send: Option<bool>, use_modifier_to_send: Option<bool>,
@ -442,10 +442,6 @@ impl Settings for AgentSettings {
&mut settings.inline_alternatives, &mut settings.inline_alternatives,
value.inline_alternatives.clone(), value.inline_alternatives.clone(),
); );
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
);
merge( merge(
&mut settings.notify_when_agent_waiting, &mut settings.notify_when_agent_waiting,
value.notify_when_agent_waiting, value.notify_when_agent_waiting,
@ -507,6 +503,20 @@ impl Settings for AgentSettings {
} }
} }
debug_assert_eq!(
sources.default.always_allow_tool_actions.unwrap_or(false),
false,
"For security, agent.always_allow_tool_actions should always be false in default.json. If it's true, that is a bug that should be fixed!"
);
// For security reasons, only trust the user's global settings for whether to always allow tool actions.
// If this could be overridden locally, an attacker could (e.g. by committing to source control and
// convincing you to switch branches) modify your project-local settings to disable the agent's safety checks.
settings.always_allow_tool_actions = sources
.user
.and_then(|setting| setting.always_allow_tool_actions)
.unwrap_or(false);
Ok(settings) Ok(settings)
} }

View file

@ -17,6 +17,7 @@ test-support = ["gpui/test-support", "language/test-support"]
[dependencies] [dependencies]
acp_thread.workspace = true acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true agent-client-protocol.workspace = true
agent.workspace = true agent.workspace = true
agent2.workspace = true agent2.workspace = true
@ -49,7 +50,6 @@ fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
html_to_markdown.workspace = true html_to_markdown.workspace = true
http_client.workspace = true http_client.workspace = true
indexed_docs.workspace = true
indoc.workspace = true indoc.workspace = true
inventory.workspace = true inventory.workspace = true
itertools.workspace = true itertools.workspace = true
@ -92,6 +92,7 @@ time.workspace = true
time_format.workspace = true time_format.workspace = true
ui.workspace = true ui.workspace = true
ui_input.workspace = true ui_input.workspace = true
url.workspace = true
urlencoding.workspace = true urlencoding.workspace = true
util.workspace = true util.workspace = true
uuid.workspace = true uuid.workspace = true
@ -101,6 +102,9 @@ workspace.workspace = true
zed_actions.workspace = true zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
assistant_context = { workspace = true, features = ["test-support"] }
assistant_tools.workspace = true assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] }

View file

@ -1,6 +1,10 @@
mod completion_provider; mod completion_provider;
mod message_history; mod entry_view_state;
mod message_editor;
mod model_selector;
mod model_selector_popover;
mod thread_view; mod thread_view;
pub use message_history::MessageHistory; pub use model_selector::AcpModelSelector;
pub use model_selector_popover::AcpModelSelectorPopover;
pub use thread_view::AcpThreadView; pub use thread_view::AcpThreadView;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,444 @@
use std::ops::Range;
use acp_thread::{AcpThread, AgentThreadEntry};
use agent::{TextThreadStore, ThreadStore};
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, TextStyleRefinement,
WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use project::Project;
use settings::Settings as _;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{Context, TextSize};
use workspace::Workspace;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
pub struct EntryViewState {
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
entries: Vec<Entry>,
}
impl EntryViewState {
pub fn new(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
) -> Self {
Self {
workspace,
project,
thread_store,
text_thread_store,
entries: Vec::new(),
}
}
pub fn entry(&self, index: usize) -> Option<&Entry> {
self.entries.get(index)
}
pub fn sync_entry(
&mut self,
index: usize,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(thread_entry) = thread.read(cx).entries().get(index) else {
return;
};
match thread_entry {
AgentThreadEntry::UserMessage(message) => {
let has_id = message.id.is_some();
let chunks = message.chunks.clone();
let message_editor = cx.new(|cx| {
let mut editor = MessageEditor::new(
self.workspace.clone(),
self.project.clone(),
self.thread_store.clone(),
self.text_thread_store.clone(),
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
},
window,
cx,
);
if !has_id {
editor.set_read_only(true, cx);
}
editor.set_message(chunks, window, cx);
editor
});
cx.subscribe(&message_editor, move |_, editor, event, cx| {
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::MessageEditorEvent(editor, *event),
})
})
.detach();
self.set_entry(index, Entry::UserMessage(message_editor));
}
AgentThreadEntry::ToolCall(tool_call) => {
let terminals = tool_call.terminals().cloned().collect::<Vec<_>>();
let diffs = tool_call.diffs().cloned().collect::<Vec<_>>();
let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) {
views
} else {
self.set_entry(index, Entry::empty());
let Some(Entry::Content(views)) = self.entries.get_mut(index) else {
unreachable!()
};
views
};
for terminal in terminals {
views.entry(terminal.entity_id()).or_insert_with(|| {
create_terminal(
self.workspace.clone(),
self.project.clone(),
terminal.clone(),
window,
cx,
)
.into_any()
});
}
for diff in diffs {
views
.entry(diff.entity_id())
.or_insert_with(|| create_editor_diff(diff.clone(), window, cx).into_any());
}
}
AgentThreadEntry::AssistantMessage(_) => {
if index == self.entries.len() {
self.entries.push(Entry::empty())
}
}
};
}
fn set_entry(&mut self, index: usize, entry: Entry) {
if index == self.entries.len() {
self.entries.push(entry);
} else {
self.entries[index] = entry;
}
}
pub fn remove(&mut self, range: Range<usize>) {
self.entries.drain(range);
}
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
diff_editor.update(cx, |diff_editor, cx| {
diff_editor.set_text_style_refinement(
diff_editor_text_style_refinement(cx),
);
cx.notify();
})
}
}
}
}
}
}
}
impl EventEmitter<EntryViewEvent> for EntryViewState {}
pub struct EntryViewEvent {
pub entry_index: usize,
pub view_event: ViewEvent,
}
pub enum ViewEvent {
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
}
pub enum Entry {
UserMessage(Entity<MessageEditor>),
Content(HashMap<EntityId, AnyEntity>),
}
impl Entry {
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self {
Self::UserMessage(editor) => Some(editor),
Entry::Content(_) => None,
}
}
pub fn editor_for_diff(&self, diff: &Entity<acp_thread::Diff>) -> Option<Entity<Editor>> {
self.content_map()?
.get(&diff.entity_id())
.cloned()
.map(|entity| entity.downcast::<Editor>().unwrap())
}
pub fn terminal(
&self,
terminal: &Entity<acp_thread::Terminal>,
) -> Option<Entity<TerminalView>> {
self.content_map()?
.get(&terminal.entity_id())
.cloned()
.map(|entity| entity.downcast::<TerminalView>().unwrap())
}
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
match self {
Self::Content(map) => Some(map),
_ => None,
}
}
fn empty() -> Self {
Self::Content(HashMap::default())
}
#[cfg(test)]
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
Self::UserMessage(_) => false,
}
}
}
fn create_terminal(
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
terminal: Entity<acp_thread::Terminal>,
window: &mut Window,
cx: &mut App,
) -> Entity<TerminalView> {
cx.new(|cx| {
let mut view = TerminalView::new(
terminal.read(cx).inner().clone(),
workspace.clone(),
None,
project.downgrade(),
window,
cx,
);
view.set_embedded_mode(Some(1000), cx);
view
})
}
fn create_editor_diff(
diff: Entity<acp_thread::Diff>,
window: &mut Window,
cx: &mut App,
) -> Entity<Editor> {
cx.new(|cx| {
let mut editor = Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
sized_by_content: true,
},
diff.read(cx).multibuffer().clone(),
None,
window,
cx,
);
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_expand_excerpt_buttons(cx);
editor.set_show_vertical_scrollbar(false, cx);
editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
editor.set_soft_wrap_mode(SoftWrap::None, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_breakpoints(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_expand_all_diff_hunks(cx);
editor.set_text_style_refinement(diff_editor_text_style_refinement(cx));
editor
})
}
fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
TextStyleRefinement {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.into(),
),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use std::{path::Path, rc::Rc};
use acp_thread::{AgentConnection, StubAgentConnection};
use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::{EditorSettings, RowInfo};
use fs::FakeFs;
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
use crate::acp::entry_view_state::EntryViewState;
use multi_buffer::MultiBufferRow;
use pretty_assertions::assert_matches;
use project::Project;
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use theme::ThemeSettings;
use util::path;
use workspace::Workspace;
#[gpui::test]
async fn test_diff_sync(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
"hello.txt": "hi world"
}),
)
.await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let tool_call = acp::ToolCall {
id: acp::ToolCallId("tool".into()),
title: "Tool call".into(),
kind: acp::ToolKind::Other,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/hello.txt".into(),
old_text: Some("hi world".into()),
new_text: "hello world".into(),
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
};
let connection = Rc::new(StubAgentConnection::new());
let thread = cx
.update(|_, cx| {
connection
.clone()
.new_thread(project.clone(), Path::new(path!("/project")), cx)
})
.await
.unwrap();
let session_id = thread.update(cx, |thread, _| thread.session_id().clone());
cx.update(|_, cx| {
connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx)
});
let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let view_state = cx.new(|_cx| {
EntryViewState::new(
workspace.downgrade(),
project.clone(),
thread_store,
text_thread_store,
)
});
view_state.update_in(cx, |view_state, window, cx| {
view_state.sync_entry(0, &thread, window, cx)
});
let diff = thread.read_with(cx, |thread, _cx| {
thread
.entries()
.get(0)
.unwrap()
.diffs()
.next()
.unwrap()
.clone()
});
cx.run_until_parked();
let diff_editor = view_state.read_with(cx, |view_state, _cx| {
view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap()
});
assert_eq!(
diff_editor.read_with(cx, |editor, cx| editor.text(cx)),
"hi world\nhello world"
);
let row_infos = diff_editor.read_with(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx);
multibuffer
.snapshot(cx)
.row_infos(MultiBufferRow(0))
.collect::<Vec<_>>()
});
assert_matches!(
row_infos.as_slice(),
[
RowInfo {
multibuffer_row: Some(MultiBufferRow(0)),
diff_status: Some(DiffHunkStatus {
kind: DiffHunkStatusKind::Deleted,
..
}),
..
},
RowInfo {
multibuffer_row: Some(MultiBufferRow(1)),
diff_status: Some(DiffHunkStatus {
kind: DiffHunkStatusKind::Added,
..
}),
..
}
]
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,92 +0,0 @@
pub struct MessageHistory<T> {
items: Vec<T>,
current: Option<usize>,
}
impl<T> Default for MessageHistory<T> {
fn default() -> Self {
MessageHistory {
items: Vec::new(),
current: None,
}
}
}
impl<T> MessageHistory<T> {
pub fn push(&mut self, message: T) {
self.current.take();
self.items.push(message);
}
pub fn reset_position(&mut self) {
self.current.take();
}
pub fn prev(&mut self) -> Option<&T> {
if self.items.is_empty() {
return None;
}
let new_ix = self
.current
.get_or_insert(self.items.len())
.saturating_sub(1);
self.current = Some(new_ix);
self.items.get(new_ix)
}
pub fn next(&mut self) -> Option<&T> {
let current = self.current.as_mut()?;
*current += 1;
self.items.get(*current).or_else(|| {
self.current.take();
None
})
}
#[cfg(test)]
pub fn items(&self) -> &[T] {
&self.items
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prev_next() {
let mut history = MessageHistory::default();
// Test empty history
assert_eq!(history.prev(), None);
assert_eq!(history.next(), None);
// Add some messages
history.push("first");
history.push("second");
history.push("third");
// Test prev navigation
assert_eq!(history.prev(), Some(&"third"));
assert_eq!(history.prev(), Some(&"second"));
assert_eq!(history.prev(), Some(&"first"));
assert_eq!(history.prev(), Some(&"first"));
assert_eq!(history.next(), Some(&"second"));
// Test mixed navigation
history.push("fourth");
assert_eq!(history.prev(), Some(&"fourth"));
assert_eq!(history.prev(), Some(&"third"));
assert_eq!(history.next(), Some(&"fourth"));
assert_eq!(history.next(), None);
// Test that push resets navigation
history.prev();
history.prev();
history.push("fifth");
assert_eq!(history.prev(), Some(&"fifth"));
}
}

View file

@ -0,0 +1,472 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_client_protocol as acp;
use anyhow::Result;
use collections::IndexMap;
use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use ui::{
AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window,
prelude::*, rems,
};
use util::ResultExt;
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
pub fn acp_model_selector(
session_id: acp::SessionId,
selector: Rc<dyn AgentModelSelector>,
window: &mut Window,
cx: &mut Context<AcpModelSelector>,
) -> AcpModelSelector {
let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
}
enum AcpModelPickerEntry {
Separator(SharedString),
Model(AgentModelInfo),
}
pub struct AcpModelPickerDelegate {
session_id: acp::SessionId,
selector: Rc<dyn AgentModelSelector>,
filtered_entries: Vec<AcpModelPickerEntry>,
models: Option<AgentModelList>,
selected_index: usize,
selected_model: Option<AgentModelInfo>,
_refresh_models_task: Task<()>,
}
impl AcpModelPickerDelegate {
fn new(
session_id: acp::SessionId,
selector: Rc<dyn AgentModelSelector>,
window: &mut Window,
cx: &mut Context<AcpModelSelector>,
) -> Self {
let mut rx = selector.watch(cx);
let refresh_models_task = cx.spawn_in(window, {
let session_id = session_id.clone();
async move |this, cx| {
async fn refresh(
this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
session_id: &acp::SessionId,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let (models_task, selected_model_task) = this.update(cx, |this, cx| {
(
this.delegate.selector.list_models(cx),
this.delegate.selector.selected_model(session_id, cx),
)
})?;
let (models, selected_model) = futures::join!(models_task, selected_model_task);
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
this.delegate.update_matches(this.query(cx), window, cx)
})?
.await;
Ok(())
}
refresh(&this, &session_id, cx).await.log_err();
while let Ok(()) = rx.recv().await {
refresh(&this, &session_id, cx).await.log_err();
}
}
});
Self {
session_id,
selector,
filtered_entries: Vec::new(),
models: None,
selected_model: None,
selected_index: 0,
_refresh_models_task: refresh_models_task,
}
}
pub fn active_model(&self) -> Option<&AgentModelInfo> {
self.selected_model.as_ref()
}
}
impl PickerDelegate for AcpModelPickerDelegate {
type ListItem = AnyElement;
fn match_count(&self) -> usize {
self.filtered_entries.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
}
fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
match self.filtered_entries.get(ix) {
Some(AcpModelPickerEntry::Model(_)) => true,
Some(AcpModelPickerEntry::Separator(_)) | None => false,
}
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select a model…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
cx.spawn_in(window, async move |this, cx| {
let filtered_models = match this
.read_with(cx, |this, cx| {
this.delegate.models.clone().map(move |models| {
fuzzy_search(models, query, cx.background_executor().clone())
})
})
.ok()
.flatten()
{
Some(task) => task.await,
None => AgentModelList::Flat(vec![]),
};
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models).collect();
// Finds the currently selected model in the list
let new_index = this
.delegate
.selected_model
.as_ref()
.and_then(|selected| {
this.delegate.filtered_entries.iter().position(|entry| {
if let AcpModelPickerEntry::Model(model_info) = entry {
model_info.id == selected.id
} else {
false
}
})
})
.unwrap_or(0);
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if let Some(AcpModelPickerEntry::Model(model_info)) =
self.filtered_entries.get(self.selected_index)
{
self.selector
.select_model(self.session_id.clone(), model_info.id.clone(), cx)
.detach_and_log_err(cx);
self.selected_model = Some(model_info.clone());
let current_index = self.selected_index;
self.set_selected_index(current_index, window, cx);
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
AcpModelPickerEntry::Separator(title) => Some(
div()
.px_2()
.pb_1()
.when(ix > 1, |this| {
this.mt_1()
.pt_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
})
.child(
Label::new(title)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.into_any_element(),
),
AcpModelPickerEntry::Model(model_info) => {
let is_selected = Some(model_info) == self.selected_model.as_ref();
let model_icon_color = if is_selected {
Color::Accent
} else {
Color::Muted
};
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.start_slot::<Icon>(model_info.icon.map(|icon| {
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
}))
.child(
h_flex()
.w_full()
.pl_0p5()
.gap_1p5()
.w(px(240.))
.child(Label::new(model_info.name.clone()).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
.size(IconSize::Small),
)
}))
.into_any_element(),
)
}
}
}
fn render_footer(
&self,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
Some(
h_flex()
.w_full()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.p_1()
.gap_4()
.justify_between()
.child(
Button::new("configure", "Configure")
.icon(IconName::Settings)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
}),
)
.into_any(),
)
}
}
fn info_list_to_picker_entries(
model_list: AgentModelList,
) -> impl Iterator<Item = AcpModelPickerEntry> {
match model_list {
AgentModelList::Flat(list) => {
itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model))
}
AgentModelList::Grouped(index_map) => {
itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| {
std::iter::once(AcpModelPickerEntry::Separator(group_name.0))
.chain(models.into_iter().map(AcpModelPickerEntry::Model))
}))
}
}
}
async fn fuzzy_search(
model_list: AgentModelList,
query: String,
executor: BackgroundExecutor,
) -> AgentModelList {
async fn fuzzy_search_list(
model_list: Vec<AgentModelInfo>,
query: &str,
executor: BackgroundExecutor,
) -> Vec<AgentModelInfo> {
let candidates = model_list
.iter()
.enumerate()
.map(|(ix, model)| {
StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name))
})
.collect::<Vec<_>>();
let mut matches = match_strings(
&candidates,
&query,
false,
true,
100,
&Default::default(),
executor,
)
.await;
matches.sort_unstable_by_key(|mat| {
let candidate = &candidates[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), candidate.id)
});
matches
.into_iter()
.map(|mat| model_list[mat.candidate_id].clone())
.collect()
}
match model_list {
AgentModelList::Flat(model_list) => {
AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await)
}
AgentModelList::Grouped(index_map) => {
let groups =
futures::future::join_all(index_map.into_iter().map(|(group_name, models)| {
fuzzy_search_list(models, &query, executor.clone())
.map(|results| (group_name, results))
}))
.await;
AgentModelList::Grouped(IndexMap::from_iter(
groups
.into_iter()
.filter(|(_, results)| !results.is_empty()),
))
}
}
}
#[cfg(test)]
mod tests {
use gpui::TestAppContext;
use super::*;
fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList {
AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map(
|(group, models)| {
(
acp_thread::AgentModelGroupName(group.to_string().into()),
models
.into_iter()
.map(|model| acp_thread::AgentModelInfo {
id: acp_thread::AgentModelId(model.to_string().into()),
name: model.to_string().into(),
icon: None,
})
.collect::<Vec<_>>(),
)
},
)))
}
fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) {
let AgentModelList::Grouped(groups) = result else {
panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result);
};
assert_eq!(
groups.len(),
expected.len(),
"Number of groups doesn't match"
);
for (i, (expected_group, expected_models)) in expected.iter().enumerate() {
let (actual_group, actual_models) = groups.get_index(i).unwrap();
assert_eq!(
actual_group.0.as_ref(),
*expected_group,
"Group at position {} doesn't match expected group",
i
);
assert_eq!(
actual_models.len(),
expected_models.len(),
"Number of models in group {} doesn't match",
expected_group
);
for (j, expected_model_name) in expected_models.iter().enumerate() {
assert_eq!(
actual_models[j].name, *expected_model_name,
"Model at position {} in group {} doesn't match expected model",
j, expected_group
);
}
}
}
#[gpui::test]
async fn test_fuzzy_match(cx: &mut TestAppContext) {
let models = create_model_list(vec![
(
"zed",
vec![
"Claude 3.7 Sonnet",
"Claude 3.7 Sonnet Thinking",
"gpt-4.1",
"gpt-4.1-nano",
],
),
("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
("ollama", vec!["mistral", "deepseek"]),
]);
// Results should preserve models order whenever possible.
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
// so it should appear first in the results.
let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
assert_models_eq(
results,
vec![
("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
],
);
// Fuzzy search
let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
assert_models_eq(
results,
vec![
("zed", vec!["gpt-4.1-nano"]),
("openai", vec!["gpt-4.1-nano"]),
],
);
}
}

View file

@ -0,0 +1,85 @@
use std::rc::Rc;
use acp_thread::AgentModelSelector;
use agent_client_protocol as acp;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*,
};
use zed_actions::agent::ToggleModelSelector;
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
pub struct AcpModelSelectorPopover {
selector: Entity<AcpModelSelector>,
menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle,
}
impl AcpModelSelectorPopover {
pub(crate) fn new(
session_id: acp::SessionId,
selector: Rc<dyn AgentModelSelector>,
menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
Self {
selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)),
menu_handle,
focus_handle,
}
}
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
self.menu_handle.toggle(window, cx);
}
}
impl Render for AcpModelSelectorPopover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let model = self.selector.read(cx).delegate.active_model();
let model_name = model
.as_ref()
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("Select a Model"));
let model_icon = model.as_ref().and_then(|model| model.icon);
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(model_icon, |this, icon| {
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
})
.child(
Label::new(model_name)
.color(Color::Muted)
.size(LabelSize::Small)
.ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",
&ToggleModelSelector,
&focus_handle,
window,
cx,
)
},
gpui::Corner::BottomRight,
cx,
)
.with_handle(self.menu_handle.clone())
.render(window, cx)
}
}

File diff suppressed because it is too large Load diff

View file

@ -465,7 +465,7 @@ impl AgentConfiguration {
"modifier-send", "modifier-send",
"Use modifier to submit a message", "Use modifier to submit a message",
Some( Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(), "Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
), ),
use_modifier_to_send, use_modifier_to_send,
move |state, _window, cx| { move |state, _window, cx| {
@ -1035,7 +1035,6 @@ fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool
&& manifest.grammars.is_empty() && manifest.grammars.is_empty()
&& manifest.language_servers.is_empty() && manifest.language_servers.is_empty()
&& manifest.slash_commands.is_empty() && manifest.slash_commands.is_empty()
&& manifest.indexed_docs_providers.is_empty()
&& manifest.snippets.is_none() && manifest.snippets.is_none()
&& manifest.debug_locators.is_empty() && manifest.debug_locators.is_empty()
} }

View file

@ -1,9 +1,9 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
use acp_thread::{AcpThread, AcpThreadEvent}; use acp_thread::{AcpThread, AcpThreadEvent};
use action_log::ActionLog;
use agent::{Thread, ThreadEvent, ThreadSummary}; use agent::{Thread, ThreadEvent, ThreadSummary};
use agent_settings::AgentSettings; use agent_settings::AgentSettings;
use anyhow::Result; use anyhow::Result;
use assistant_tool::ActionLog;
use buffer_diff::DiffHunkStatus; use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use editor::{ use editor::{
@ -1521,7 +1521,8 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx); self.update_reviewing_editors(workspace, window, cx);
} }
} }
AcpThreadEvent::Stopped AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::Stopped
| AcpThreadEvent::ToolAuthorizationRequired | AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::Error | AcpThreadEvent::Error
| AcpThreadEvent::ServerExited(_) => {} | AcpThreadEvent::ServerExited(_) => {}

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,6 @@ mod agent_diff;
mod agent_model_selector; mod agent_model_selector;
mod agent_panel; mod agent_panel;
mod buffer_codegen; mod buffer_codegen;
mod burn_mode_tooltip;
mod context_picker; mod context_picker;
mod context_server_configuration; mod context_server_configuration;
mod context_strip; mod context_strip;
@ -64,6 +63,8 @@ actions!(
NewTextThread, NewTextThread,
/// Toggles the context picker interface for adding files, symbols, or other context. /// Toggles the context picker interface for adding files, symbols, or other context.
ToggleContextPicker, ToggleContextPicker,
/// Toggles the menu to create new agent threads.
ToggleNewThreadMenu,
/// Toggles the navigation menu for switching between threads and views. /// Toggles the navigation menu for switching between threads and views.
ToggleNavigationMenu, ToggleNavigationMenu,
/// Toggles the options menu for agent settings and preferences. /// Toggles the options menu for agent settings and preferences.
@ -155,11 +156,11 @@ enum ExternalAgent {
} }
impl ExternalAgent { impl ExternalAgent {
pub fn server(&self) -> Rc<dyn agent_servers::AgentServer> { pub fn server(&self, fs: Arc<dyn fs::Fs>) -> Rc<dyn agent_servers::AgentServer> {
match self { match self {
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer), ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs)),
} }
} }
} }
@ -241,7 +242,6 @@ pub fn init(
client.telemetry().clone(), client.telemetry().clone(),
cx, cx,
); );
indexed_docs::init(cx);
cx.observe_new(move |workspace, window, cx| { cx.observe_new(move |workspace, window, cx| {
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx) ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
}) })
@ -408,12 +408,6 @@ fn update_slash_commands_from_settings(cx: &mut App) {
let slash_command_registry = SlashCommandRegistry::global(cx); let slash_command_registry = SlashCommandRegistry::global(cx);
let settings = SlashCommandSettings::get_global(cx); let settings = SlashCommandSettings::get_global(cx);
if settings.docs.enabled {
slash_command_registry.register_command(assistant_slash_commands::DocsSlashCommand, true);
} else {
slash_command_registry.unregister_command(assistant_slash_commands::DocsSlashCommand);
}
if settings.cargo_workspace.enabled { if settings.cargo_workspace.enabled {
slash_command_registry slash_command_registry
.register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true); .register_command(assistant_slash_commands::CargoWorkspaceSlashCommand, true);

View file

@ -1,61 +0,0 @@
use gpui::{Context, FontWeight, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct BurnModeTooltip {
selected: bool,
}
impl BurnModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
}
impl Render for BurnModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (icon, color) = if self.selected {
(IconName::ZedBurnModeOn, Color::Error)
} else {
(IconName::ZedBurnMode, Color::Default)
};
let turned_on = h_flex()
.h_4()
.px_1()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().text_accent.opacity(0.1))
.rounded_sm()
.child(
Label::new("ON")
.size(LabelSize::XSmall)
.weight(FontWeight::SEMIBOLD)
.color(Color::Accent),
);
let title = h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::Small).color(color))
.child(Label::new("Burn Mode"))
.when(self.selected, |title| title.child(turned_on));
tooltip_container(window, cx, |this, _, _| {
this
.child(title)
.child(
div()
.max_w_64()
.child(
Label::new("Enables models to use large context windows, unlimited tool calls, and other capabilities for expanded reasoning.")
.size(LabelSize::Small)
.color(Color::Muted)
)
)
})
}
}

Some files were not shown because too many files have changed in this diff Show more