Compare commits

..

57 Commits

Author SHA1 Message Date
Michael Sloan
b38b6ff12c agent checkpoint dbgs 2025-08-12 01:11:38 -06:00
Conrad Irwin
52a9101970 vim: Add ctrl-y/e in insert mode (#36017)
Closes #17292

Release Notes:

- vim: Added ctrl-y/ctrl-e in insert mode to copy the next character
from the line above or below
2025-08-11 23:20:09 -06:00
Conrad Irwin
1a798830cb Fix running vim tests with --features neovim (#36014)
This was broken incidentally in
https://github.com/zed-industries/zed/pull/33417

A better fix would be to fix app shutdown to take control of the
executor so that we *can* run
foreground tasks; but that is a bit fiddly (draft #36015) 

Release Notes:

- N/A
2025-08-12 05:08:58 +00:00
Kirill Bulatov
481e3e5092 Ignore capability registrations with empty capabilities (#36000) 2025-08-12 07:53:20 +03:00
Matt
b35e69692d docs: Add a missing comma in Rust debugging JSON (#36007)
Update the Rust debugging doc to include a missing comma in one of the
example JSON's.
2025-08-12 03:06:02 +00:00
Conrad Irwin
add67bde43 Remove unnecessary argument from Vim#update_editor (#36001)
Release Notes:

- N/A
2025-08-11 16:10:06 -06:00
Victor Tran
fa3d0aaed4 gpui: Allow selection of "Services" menu independent of menu title (#34115)
Release Notes:

- N/A

---

In the same vein as #29538, the "Services" menu on macOS depended on the
text being exactly "Services", not allowing for i18n of the menu name.

This PR introduces a new menu type called `OsMenu` that defines a
special menu that can be populated by the system. Currently, it takes
one enum value, `ServicesMenu` that tells the system to populate its
contents with the items it would usually populate the "Services" menu
with.

An example of this being used has been implemented in the `set_menus`
example:
`cargo run -p gpui --example set_menus`

---

Point to consider:

In `mac/platform.rs:414` the existing code for setting the "Services"
menu remains for backwards compatibility. Should this remain now that
this new method exists to set the menu, or should it be removed?

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-08-11 21:10:14 +00:00
Danilo Leal
094e878ccf agent2: Refine terminal tool call display (#35984)
Release Notes:

- N/A
2025-08-11 17:50:47 -03:00
Joseph T. Lyons
54d4665100 Add windows issue template (#35998)
Release Notes:

- N/A
2025-08-11 19:25:18 +00:00
localcc
2c84e33b7b Fix icon padding (#35990)
Release Notes:

- N/A
2025-08-11 19:57:39 +02:00
Bennet Bo Fenner
bb6ea22944 agent2: Port more tools (#35987)
Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-11 17:24:48 +00:00
Antonio Scandurra
365b5aa31d Centralize always_allow logic when authorizing agent2 tools (#35988)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-08-11 17:22:19 +00:00
localcc
56c4992b9a Fix underline flickering (#35989)
Closes #35559

Release Notes:

- Fixed underline flickering
2025-08-11 19:17:48 +02:00
Cole Miller
76b95d4f67 Try to diagnose memory access violation in Windows tests (#35926)
Release Notes:

- N/A
2025-08-11 17:06:31 +00:00
Piotr Osiewicz
39dfd52d04 python: Create DAP download directory sooner (#35986)
Closes #35980

Release Notes:

- Fixed Python Debug sessions not starting up when a session is started
up for the first time.
2025-08-11 16:50:24 +00:00
Cole Miller
42bf5a17b9 Delay rendering tool call diff editor until it has a revealed range (#35901)
Release Notes:

- N/A
2025-08-11 12:49:46 -04:00
Anthony Eid
7965052757 Make SwitchField component clickable from the keyboard when focused (#35830)
Release Notes:

- N/A
2025-08-11 16:33:21 +00:00
Anthony Eid
62270b33c2 git: Add ability to clone remote repositories from Zed (#35606)
This PR adds preliminary git clone support through using the new
`GitClone` action. This works with SSH connections too.

- [x] Get backend working
- [x] Add a UI to interact with this

Future follow-ups:
- Polish the UI
- Have the path select prompt say "Select Repository clone target"
instead of “Open”
- Use Zed path prompt if the user has that as a setting
- Add support for cloning from a user's GitHub repositories directly

Release Notes:

- Add the ability to clone remote git repositories through the `git:
Clone` action

---------

Co-authored-by: hpmcdona <hayden_mcdonald@brown.edu>
2025-08-11 15:09:38 +00:00
Conrad Irwin
12084b6677 Fix keys not being sent to terminal (#35979)
Fixes #35057

Release Notes:

- Fix input being sent to editor/terminal when pending keystrokes are
resolved
2025-08-11 09:07:32 -06:00
Richard Feldman
6478e66e7a Stricter disable_ai overrides (#35977)
Settings overrides (e.g. local project settings, server settings) can no
longer change `disable_ai` to `false` if it was `true`; they can only
change it to `true`. In other words, settings can only cause AI to be
*more* disabled, they can't undo the user's preference for no AI (or the
project's requirement not to use AI).

Release Notes:

- Settings overrides (such as local project settings) can now only
override `disable_ai` to become `true`; they can no longer cause
otherwise-disabled AI to become re-enabled.

---------

Co-authored-by: Assistant <assistant@anthropic.com>
Co-authored-by: David Kleingeld <git@davidsk.dev>
2025-08-11 10:56:45 -04:00
Richard Feldman
abb64d2320 Ignore project-local settings for always_allow_tool_actions (#35976)
Now `always_allow_tool_actions` is only respected as the user's global
setting, not as an overridable project-local setting. This way, you
don't have to worry about switching into a project (or switching
branches within a project) and discovering that suddenly your tool calls
no longer require confirmation.

Release Notes:

- Removed always_allow_tool_actions from project-local settings (it is
now global-only)

Co-authored-by: David Kleingeld <git@davidsk.dev>
2025-08-11 14:09:25 +00:00
Ben Brandt
8dbded46d8 agent2: Add now, grep, and web search tools (#35974)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-11 15:34:34 +02:00
Antonio Scandurra
ebcce8730d Port some more tools to agent2 (#35973)
Release Notes:

- N/A
2025-08-11 15:10:46 +02:00
Oleksiy Syvokon
d5ed569fad zeta: Reduce request payload (#35968)
1. Don't send diagnostics if there are more than 10 of them. This fixes
an issue with sending 100kb requests for projects with many warnings.

2. Don't send speculated_output and outline, as those are currently
unused.


Release Notes:

- Improved edit prediction latency
2025-08-11 15:33:16 +03:00
Lukas Wirth
a88c533ffc language: Fix rust-analyzer removing itself on download (#35971)
Release Notes:

- N/A\
2025-08-11 12:24:53 +00:00
localcc
702a95ffb2 Fix underline DPI (#35816)
Release Notes:

- Fixed wavy underlines looking inconsistent on different displays
2025-08-11 13:57:30 +02:00
Antonio Scandurra
086ea3c619 Port terminal tool to agent2 (#35918)
Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-08-11 10:31:13 +00:00
smit
422e0a2eb7 project: Add more dynamic capability registrations for LSP (#35306)
Closes #34204

Adds the ability to dynamically register and unregister code actions for
language servers such as Biome.

See more:
https://github.com/zed-industries/zed/issues/34204#issuecomment-3134227856

Release Notes:

- Fixed an issue where the Biome formatter was always used even when
`require_config_file` was set to true and the project had no config
file.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-08-11 09:59:41 +00:00
Lukas Wirth
e132c7cad9 dap_adapters: Log CodeLldb version fetching errors (#35943)
Release Notes:

- N/A
2025-08-11 08:15:59 +00:00
Lukas Wirth
8d332da4c5 languages: Don't remove old artifacts on download failure (#35967)
Release Notes:

- N/A
2025-08-11 07:20:03 +00:00
Danilo Leal
c82cd0c6b1 docs: Clarify storage of AI API keys (#35963)
Previous docs was inaccurate as Zed doesn't store LLM API keys in the
`settings.json`.

Release Notes:

- N/A
2025-08-10 23:28:28 -03:00
Ben Brandt
308cb9e537 Pull action_log into its own crate (#35959)
Release Notes:

- N/A
2025-08-10 21:57:55 +00:00
jingyuexing
72761797a2 Fix SHA-256 verification mismatch when downloading language servers (#35953)
Closes #35642 

Release Notes:

- Fixed: when the expected digest included a "sha256:" prefix while the
computed
digest has no prefix.
2025-08-10 21:40:14 +02:00
Kirill Bulatov
6bd2f8758e Simplify the lock usage (#35957)
Follow-up of https://github.com/zed-industries/zed/pull/35955

Release Notes:

- N/A

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2025-08-10 19:32:25 +00:00
Danilo Leal
f3d6deb5a3 debugger: Add refinements to the UI (#35940)
Took a little bit of time to add just a handful of small tweaks to the
debugger UI so it looks slightly more polished. This PR includes
adjustments to size, focus styles, and more in icon buttons, overall
spacing nudges in each section pane, making tooltip labels title case
(for overall consistency), and some icon SVG iteration.

Release Notes:

- N/A
2025-08-10 15:23:27 -03:00
Kirill Bulatov
95e302fa68 Properly use static instead of const for global types that need a single init (#35955)
Release Notes:

- N/A
2025-08-10 18:01:54 +00:00
Lukas Wirth
9cd5c3656e util: Fix crate name extraction for log_error_with_caller (#35944)
The paths can be absolute, meaning they would just log the initial
segment of where the repo was cloned.

Release Notes:

- N/A
2025-08-10 15:19:06 +00:00
Oleksiy Syvokon
8382afb2ba evals: Run unit evals CI weekly (#35950)
Release Notes:

- N/A
2025-08-10 14:43:48 +00:00
Danilo Leal
2d9cd2ac88 Update and refine some icons (#35938)
Follow up to https://github.com/zed-industries/zed/pull/35856.

Release Notes:

- N/A
2025-08-09 22:12:23 -03:00
Piotr Osiewicz
daa53f2761 Revert "Revert "chore: Bump Rust to 1.89 (#35788)"" (#35937)
Reverts zed-industries/zed#35843

Docker image for 1.89 is now up.
2025-08-09 23:48:58 +02:00
Ben Brandt
5901aec40a agent2: Remove model param from Thread::send method (#35936)
It instead uses the currently selected model

Release Notes:

- N/A
2025-08-09 21:40:44 +00:00
Umesh Yadav
ce39644cbd language_models: Add thinking to Mistral Provider (#32476)
Tested prompt:

John is one of 4 children. The first sister is 4 years old. Next year,
the second sister will be twice as old as the first sister. The third
sister is two years older than the second sister. The third sister is
half the age of her older brother. How old is John? Return your thinking
inside <think></think>

Release Notes:

- Add thinking to Mistral Provider

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-08-09 15:25:47 -04:00
Julia Ryan
021681d456 Don't generate crash reports on the Dev channel (#35915)
We only want minidumps to be generated on actual release builds. Now we
avoid spawning crash handler processes for dev builds. To test
minidumping you can still set the `ZED_GENERATE_MINIDUMPS` env var which
force-enable the feature.

Release Notes:

- N/A
2025-08-09 11:42:30 +00:00
Julia Ryan
7862c0c945 Add more info to crash reports (#35914)
None of this is new info, we're just pulling more things out of the
panic message to send with the minidump. We do want to add more fields
like gpu version which will come in a subsequent change.

Release Notes:

- N/A
2025-08-09 11:20:38 +00:00
Julia Ryan
c91fb4caf4 Add sentry release step to ci (#35911)
This should allow us to associate sha's from crashes and generate links
to github source in sentry.

Release Notes:

- N/A
2025-08-09 10:37:28 +00:00
Julia Ryan
4c5058c077 Fix uploading mac dsyms (#35904)
I'm not sure we actually want to be using `debug-info=unpacked` and then
running `dsymutil` with `--flat`, but for now the minimal change to get
this working is to manually specify the flattened, uncompressed debug
info file for upload, which in turn will cause `sentry-cli` to pick up
on source-info for the zed binary.

I think in the future we should switch to `packed` debug info, both for
the zed binary _and_ the remote server, and then we can tar up the
better supported `dSYM` folder format rather than the flat dwarf
version.

Release Notes:

- N/A
2025-08-09 03:28:36 -07:00
Michael Sloan
4e97968bcb zeta: Update data collection eligibility when license file contents change + add Apache 2.0 (#35900)
Closes #35070

Release Notes:

- Edit Prediction: Made license detection update eligibility for data
collection when license files change.
- Edit Prediction: Added Apache 2.0 license to opensource licenses
eligible for data collection.
- Edit Prediction: Made license detection less sensitive to whitespace
differences and check more files.
2025-08-09 00:38:54 +00:00
Cole Miller
c053923015 thread_view: Trim only trailing whitespace from last chunk of user message (#35902)
This fixes internal whitespace after the last @mention going missing
from the user message as displayed in history.

Release Notes:

- N/A
2025-08-08 23:50:59 +00:00
Michael Sloan
aedf195e97 Use distinct user agents in agent eval and zeta-cli (#35897)
Agent eval now also uses a proper Zed version

Release Notes:

- N/A
2025-08-08 23:26:38 +00:00
Alejandro Fernández Gómez
9443c930de Make One Dark's ansi.*magenta colors more magenta-y (#35423)
Tweak the `ansi.*magenta` colours so they are not confused with
`ansi.*red`. This matches how "One Light" behaves, where `ansi.*magenta`
uses the same purple as for keyword.

This change helps distinguish anything that the terminal might use
magenta for from errors, and helps make more readable the output of
certain tools.

For maintainers: The color for `ansi.magenta` is the same as for
`syntax.keyword`. The others are modifications on that colour to taste.
If you have some specific shades that need to be used please tell me, or
feel free to take over the PR.

Before: `jj log` and `difftastic` output

<img width="863" height="592" alt="Screenshot 2025-07-31 at 19 32 11"
src="https://github.com/user-attachments/assets/994b1cbd-ff64-4620-bd51-a5073fd6eb2a"
/>

After:

<img width="862" height="558" alt="Screenshot 2025-07-31 at 19 35 33"
src="https://github.com/user-attachments/assets/49dfb856-6b63-4498-8779-b8624230d6a3"
/>

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-08-08 22:50:39 +00:00
Michael Sloan
a1bc6ee75e zeta: Only send outline and diagnostics when data collection is enabled (#35896)
This data is not currently used by edit predictions - it is only useful
when `can_collect_data == true`.

Release Notes:

- N/A
2025-08-08 22:16:13 +00:00
Phileas Lebada
a4f7747c73 Improve extension development docs (#33646)
I'm installing an extension for the first time from source and assumed
that the sentence

> If you already have a published extension with the same name
installed, your dev extension will override it.

also means that it would override the already installed extension.

Besides that I've had to use `--foreground` mode to also get more
meaningful error messages under NixOS without using
`programs.nix-ld.enabled = true;`.


Release Notes:

- Improved Zed documentation for extension development

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-08-08 21:44:03 +00:00
Julia Ryan
d7db03443a Upload debug info for preview/stable builds (#35895)
This should fix all the unsymbolicated backtraces we're seeing on
preview builds

Release Notes:

- N/A
2025-08-08 21:32:36 +00:00
Alvaro Parker
f3399daf6c file_finder: Fix right border not rendering (#35684)
Closes #35683

Release Notes:

- Fixed file finder borders not rendering properly

Before: 

<img width="1921" height="1081" alt="image"
src="https://github.com/user-attachments/assets/62f39bfb-3e0e-43af-a00a-a6c378b067fc"
/>

After:

<img width="1921" height="1081" alt="image"
src="https://github.com/user-attachments/assets/b9a00e4c-c126-40a4-9a63-8e44396a0e84"
/>
2025-08-08 18:32:13 -03:00
Aleksei Gusev
2be6f9d17b theme: Add support for per-theme overrides (#30860)
Closes #14050

Release Notes:

- Added the ability to set theme-specific overrides via the
`theme_overrides` setting.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-08-08 21:17:19 +00:00
Peter Tripp
c6ef35ba37 Disable edit predictions in Zed settings by default (#34401)
In Zed settings, json schema based LSP autocomplete is very good, edit
predictions are not.
Disable the latter by default.

Release Notes:

- N/A
2025-08-08 21:05:28 +00:00
Daniel Sauble
91474e247f Make close tab and pin tab buttons slightly larger for better usability (#34428)
Closes #6817

Increases the size of tab buttons from 16px to 18px so they're easier to
click.

For comparison, tab buttons in VSCode have a click target size of 20px,
so we're still a bit smaller than that.

Before:

<img width="261" height="33" alt="before_tab_buttons"
src="https://github.com/user-attachments/assets/7a43f8e1-da84-4981-b2c8-ca77f12ef279"
/>

After:

<img width="265" height="33" alt="after_tab_buttons"
src="https://github.com/user-attachments/assets/c64a98dd-c6bc-480b-be10-f7fa467074c4"
/>

VSCode (for comparison):

<img width="242" height="34" alt="Screenshot 2025-07-14 at 1 43 03 PM"
src="https://github.com/user-attachments/assets/2fafeb2f-75e6-45d1-83da-8601c22474bd"
/>

Release Notes:

- Improve usability of close tab and pin tab buttons by making them
slightly larger

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-08-08 21:04:32 +00:00
196 changed files with 8244 additions and 1941 deletions

View File

@@ -0,0 +1,35 @@
name: Bug Report (Windows)
description: Zed Windows-Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows: <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:
node-version: "18"
- name: Configure crash dumps
shell: powershell
run: |
# Record the start time for this CI run
$runStartTime = Get-Date
$runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "CI run started at: $runStartTimeStr"
# Save the timestamp for later use
echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
# Create crash dump directory in workspace (non-persistent)
$dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
Write-Host "Setting up crash dump detection..."
Write-Host "Workspace dump path: $dumpPath"
# Note: We're NOT modifying registry on stateful runners
# Instead, we'll check default Windows crash locations after tests
- name: Run tests
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: 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

@@ -526,6 +526,11 @@ jobs:
with:
node-version: "18"
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
@@ -611,6 +616,11 @@ jobs:
- name: Install Linux dependencies
run: ./script/linux && ./script/install-mold 2.34.0
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
if: startsWith(github.ref, 'refs/tags/v')
run: |
@@ -664,6 +674,11 @@ jobs:
- name: Install Linux dependencies
run: ./script/linux
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
if: startsWith(github.ref, 'refs/tags/v')
run: |
@@ -789,6 +804,11 @@ jobs:
with:
clean: false
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Determine version and release channel
working-directory: ${{ env.ZED_WORKSPACE }}
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
@@ -831,3 +851,12 @@ jobs:
run: gh release edit "$GITHUB_REF_NAME" --draft=false
env:
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 tag -f nightly
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:
schedule:
# 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:
concurrency:

55
Cargo.lock generated
View File

@@ -6,9 +6,9 @@ version = 4
name = "acp_thread"
version = "0.1.0"
dependencies = [
"action_log",
"agent-client-protocol",
"anyhow",
"assistant_tool",
"buffer_diff",
"editor",
"env_logger 0.11.8",
@@ -27,11 +27,38 @@ dependencies = [
"settings",
"smol",
"tempfile",
"terminal",
"ui",
"util",
"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]]
name = "activity_indicator"
version = "0.1.0"
@@ -84,6 +111,7 @@ dependencies = [
name = "agent"
version = "0.1.0"
dependencies = [
"action_log",
"agent_settings",
"anyhow",
"assistant_context",
@@ -156,23 +184,28 @@ name = "agent2"
version = "0.1.0"
dependencies = [
"acp_thread",
"action_log",
"agent-client-protocol",
"agent_servers",
"agent_settings",
"anyhow",
"assistant_tool",
"assistant_tools",
"chrono",
"client",
"clock",
"cloud_llm_client",
"collections",
"ctor",
"editor",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"handlebars 4.5.0",
"html_to_markdown",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
@@ -180,7 +213,9 @@ dependencies = [
"language_models",
"log",
"lsp",
"open",
"paths",
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
@@ -191,12 +226,21 @@ dependencies = [
"serde_json",
"settings",
"smol",
"task",
"tempfile",
"terminal",
"theme",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"uuid",
"watch",
"web_search",
"which 6.0.3",
"workspace-hack",
"worktree",
"zlog",
]
[[package]]
@@ -261,6 +305,7 @@ name = "agent_ui"
version = "0.1.0"
dependencies = [
"acp_thread",
"action_log",
"agent",
"agent-client-protocol",
"agent2",
@@ -842,13 +887,13 @@ dependencies = [
name = "assistant_tool"
version = "0.1.0"
dependencies = [
"action_log",
"anyhow",
"buffer_diff",
"clock",
"collections",
"ctor",
"derive_more 0.99.19",
"futures 0.3.31",
"gpui",
"icons",
"indoc",
@@ -865,7 +910,6 @@ dependencies = [
"settings",
"text",
"util",
"watch",
"workspace",
"workspace-hack",
"zlog",
@@ -875,6 +919,7 @@ dependencies = [
name = "assistant_tools"
version = "0.1.0"
dependencies = [
"action_log",
"agent_settings",
"anyhow",
"assistant_tool",
@@ -4014,6 +4059,7 @@ dependencies = [
"log",
"minidumper",
"paths",
"release_channel",
"smol",
"workspace-hack",
]
@@ -12871,7 +12917,6 @@ dependencies = [
"prost-build 0.9.0",
"serde",
"typed-path",
"util",
"workspace-hack",
]
@@ -13523,6 +13568,7 @@ dependencies = [
name = "remote_server"
version = "0.1.0"
dependencies = [
"action_log",
"anyhow",
"askpass",
"assistant_tool",
@@ -17979,6 +18025,7 @@ dependencies = [
"command_palette_hooks",
"db",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
"git_ui",
"gpui",

View File

@@ -2,6 +2,7 @@
resolver = "2"
members = [
"crates/acp_thread",
"crates/action_log",
"crates/activity_indicator",
"crates/agent",
"crates/agent2",
@@ -229,6 +230,7 @@ edition = "2024"
#
acp_thread = { path = "crates/acp_thread" }
action_log = { path = "crates/action_log" }
agent = { path = "crates/agent" }
agent2 = { path = "crates/agent2" }
activity_indicator = { path = "crates/activity_indicator" }
@@ -839,6 +841,7 @@ style = { level = "allow", priority = -1 }
module_inception = { level = "deny" }
question_mark = { level = "deny" }
redundant_closure = { level = "deny" }
declare_interior_mutable_const = { level = "deny" }
# Individual rules that have violations in the codebase:
type_complexity = "allow"
# We often return trait objects from `new` functions.

View File

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

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">
<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="M3 3V5.77778H5.77778" 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="M10.2224 10.2222H13.0002V13" 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="M12.8989 3V5.77778H10.1211" 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="M5.87891 10.2222H3.10111V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 854 B

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

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">
<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>

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 566 B

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

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">
<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>

Before

Width:  |  Height:  |  Size: 817 B

After

Width:  |  Height:  |  Size: 643 B

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">
<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="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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

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">
<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="M13 3L9 7" 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="M13 3L9.5 6.5" 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>

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 525 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">
<path d="M4 9H7V12" 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="M9 7L13 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.5 9.5H6.5V12.5" 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.5 6.5L13 3" 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>

Before

Width:  |  Height:  |  Size: 515 B

After

Width:  |  Height:  |  Size: 539 B

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.0 KiB

View File

@@ -333,10 +333,14 @@
"ctrl-x ctrl-c": "editor::ShowEditPrediction", // zed specific
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
"ctrl-y": "vim::InsertFromAbove",
"ctrl-e": "vim::InsertFromBelow",
"ctrl-k": ["vim::PushDigraph", {}],
"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.

View File

@@ -1210,7 +1210,18 @@
// Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
"disabled_globs": [
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml",
"**/.zed/settings.json", // zed project settings
"/**/zed/settings.json", // zed user settings
"/**/zed/keymap.json"
],
// When to show edit predictions previews in buffer.
// This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available.

View File

@@ -86,9 +86,9 @@
"terminal.ansi.blue": "#74ade8ff",
"terminal.ansi.bright_blue": "#385378ff",
"terminal.ansi.dim_blue": "#bed5f4ff",
"terminal.ansi.magenta": "#be5046ff",
"terminal.ansi.bright_magenta": "#5e2b26ff",
"terminal.ansi.dim_magenta": "#e6a79eff",
"terminal.ansi.magenta": "#b477cfff",
"terminal.ansi.bright_magenta": "#d6b4e4ff",
"terminal.ansi.dim_magenta": "#612a79ff",
"terminal.ansi.cyan": "#6eb4bfff",
"terminal.ansi.bright_cyan": "#3a565bff",
"terminal.ansi.dim_cyan": "#b9d9dfff",

View File

@@ -16,9 +16,9 @@ doctest = false
test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
editor.workspace = true
futures.workspace = true
@@ -32,6 +32,7 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
terminal.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,12 +1,14 @@
mod connection;
mod diff;
mod terminal;
pub use connection::*;
pub use diff::*;
pub use terminal::*;
use action_log::ActionLog;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use assistant_tool::ActionLog;
use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
@@ -147,6 +149,14 @@ impl AgentThreadEntry {
}
}
pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {
if let AgentThreadEntry::ToolCall(call) = self {
itertools::Either::Left(call.terminals())
} else {
itertools::Either::Right(std::iter::empty())
}
}
pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> {
if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self {
Some(locations)
@@ -250,8 +260,17 @@ impl ToolCall {
pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
self.content.iter().filter_map(|content| match content {
ToolCallContent::ContentBlock { .. } => None,
ToolCallContent::Diff { diff } => Some(diff),
ToolCallContent::Diff(diff) => Some(diff),
ToolCallContent::ContentBlock(_) => None,
ToolCallContent::Terminal(_) => None,
})
}
pub fn terminals(&self) -> impl Iterator<Item = &Entity<Terminal>> {
self.content.iter().filter_map(|content| match content {
ToolCallContent::Terminal(terminal) => Some(terminal),
ToolCallContent::ContentBlock(_) => None,
ToolCallContent::Diff(_) => None,
})
}
@@ -387,8 +406,9 @@ impl ContentBlock {
#[derive(Debug)]
pub enum ToolCallContent {
ContentBlock { content: ContentBlock },
Diff { diff: Entity<Diff> },
ContentBlock(ContentBlock),
Diff(Entity<Diff>),
Terminal(Entity<Terminal>),
}
impl ToolCallContent {
@@ -398,19 +418,20 @@ impl ToolCallContent {
cx: &mut App,
) -> Self {
match content {
acp::ToolCallContent::Content { content } => Self::ContentBlock {
content: ContentBlock::new(content, &language_registry, cx),
},
acp::ToolCallContent::Diff { diff } => Self::Diff {
diff: cx.new(|cx| Diff::from_acp(diff, language_registry, cx)),
},
acp::ToolCallContent::Content { content } => {
Self::ContentBlock(ContentBlock::new(content, &language_registry, cx))
}
acp::ToolCallContent::Diff { diff } => {
Self::Diff(cx.new(|cx| Diff::from_acp(diff, language_registry, cx)))
}
}
}
pub fn to_markdown(&self, cx: &App) -> String {
match self {
Self::ContentBlock { content } => content.to_markdown(cx).to_string(),
Self::Diff { diff } => diff.read(cx).to_markdown(cx),
Self::ContentBlock(content) => content.to_markdown(cx).to_string(),
Self::Diff(diff) => diff.read(cx).to_markdown(cx),
Self::Terminal(terminal) => terminal.read(cx).to_markdown(cx),
}
}
}
@@ -419,6 +440,7 @@ impl ToolCallContent {
pub enum ToolCallUpdate {
UpdateFields(acp::ToolCallUpdate),
UpdateDiff(ToolCallUpdateDiff),
UpdateTerminal(ToolCallUpdateTerminal),
}
impl ToolCallUpdate {
@@ -426,6 +448,7 @@ impl ToolCallUpdate {
match self {
Self::UpdateFields(update) => &update.id,
Self::UpdateDiff(diff) => &diff.id,
Self::UpdateTerminal(terminal) => &terminal.id,
}
}
}
@@ -448,6 +471,18 @@ pub struct ToolCallUpdateDiff {
pub diff: Entity<Diff>,
}
impl From<ToolCallUpdateTerminal> for ToolCallUpdate {
fn from(terminal: ToolCallUpdateTerminal) -> Self {
Self::UpdateTerminal(terminal)
}
}
#[derive(Debug, PartialEq)]
pub struct ToolCallUpdateTerminal {
pub id: acp::ToolCallId,
pub terminal: Entity<Terminal>,
}
#[derive(Debug, Default)]
pub struct Plan {
pub entries: Vec<PlanEntry>,
@@ -760,7 +795,13 @@ impl AcpThread {
current_call.content.clear();
current_call
.content
.push(ToolCallContent::Diff { diff: update.diff });
.push(ToolCallContent::Diff(update.diff));
}
ToolCallUpdate::UpdateTerminal(update) => {
current_call.content.clear();
current_call
.content
.push(ToolCallContent::Terminal(update.terminal));
}
}

View File

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

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

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -17,8 +17,6 @@ use util::{
pub struct ActionLog {
/// Buffers that we want to notify the model about when they change.
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
project: Entity<Project>,
}
@@ -28,7 +26,6 @@ impl ActionLog {
pub fn new(project: Entity<Project>) -> Self {
Self {
tracked_buffers: BTreeMap::default(),
edited_since_project_diagnostics_check: false,
project,
}
}
@@ -37,16 +34,6 @@ impl ActionLog {
&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> {
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
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);
}
/// 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>) {
self.edited_since_project_diagnostics_check = true;
let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx);
if let TrackedBufferStatus::Deleted = tracked_buffer.status {
tracked_buffer.status = TrackedBufferStatus::Modified;

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,10 @@ use crate::{
},
tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState},
};
use action_log::ActionLog;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
@@ -812,6 +813,7 @@ impl Thread {
}
fn finalize_pending_checkpoint(&mut self, cx: &mut Context<Self>) {
dbg!("finalize_pending_checkpoint");
let pending_checkpoint = if self.is_generating() {
return;
} else if let Some(checkpoint) = self.pending_checkpoint.take() {
@@ -828,10 +830,13 @@ impl Thread {
pending_checkpoint: ThreadCheckpoint,
cx: &mut Context<Self>,
) {
dbg!("finalize_checkpoint");
let git_store = self.project.read(cx).git_store().clone();
let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
cx.spawn(async move |this, cx| match final_checkpoint.await {
Ok(final_checkpoint) => {
dbg!(&pending_checkpoint.git_checkpoint);
dbg!(&final_checkpoint);
let equal = git_store
.update(cx, |store, cx| {
store.compare_checkpoints(
@@ -843,7 +848,7 @@ impl Thread {
.await
.unwrap_or(false);
if !equal {
if dbg!(!equal) {
this.update(cx, |this, cx| {
this.insert_checkpoint(pending_checkpoint, cx)
})?;
@@ -859,6 +864,7 @@ impl Thread {
}
fn insert_checkpoint(&mut self, checkpoint: ThreadCheckpoint, cx: &mut Context<Self>) {
dbg!("insert_checkpoint");
self.checkpoints_by_message
.insert(checkpoint.message_id, checkpoint);
cx.emit(ThreadEvent::CheckpointChanged);
@@ -866,6 +872,7 @@ impl Thread {
}
pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> {
dbg!();
self.last_restore_checkpoint.as_ref()
}

View File

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

View File

@@ -1,9 +1,13 @@
use crate::{templates::Templates, AgentResponseEvent, Thread};
use crate::{EditFileTool, FindPathTool, ReadFileTool, ThinkingTool, ToolCallAuthorization};
use crate::{AgentResponseEvent, Thread, templates::Templates};
use crate::{
CopyPathTool, CreateDirectoryTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool,
GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool,
ThinkingTool, ToolCallAuthorization, WebSearchTool,
};
use acp_thread::ModelSelector;
use agent_client_protocol as acp;
use anyhow::{anyhow, Context as _, Result};
use futures::{future, StreamExt};
use anyhow::{Context as _, Result, anyhow};
use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
@@ -414,10 +418,22 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
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(CreateDirectoryTool::new(project.clone()));
thread.add_tool(CopyPathTool::new(project.clone()));
thread.add_tool(DiagnosticsTool::new(project.clone()));
thread.add_tool(MovePathTool::new(project.clone()));
thread.add_tool(ListDirectoryTool::new(project.clone()));
thread.add_tool(OpenTool::new(project.clone()));
thread.add_tool(ThinkingTool);
thread.add_tool(FindPathTool::new(project.clone()));
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
thread.add_tool(GrepTool::new(project.clone()));
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
thread.add_tool(EditFileTool::new(cx.entity()));
thread.add_tool(NowTool);
thread.add_tool(TerminalTool::new(project.clone(), cx));
// TODO: Needs to be conditional based on zed model or not
thread.add_tool(WebSearchTool);
thread
});
@@ -491,8 +507,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
// 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))?;
let mut response_stream = thread.update(cx, |thread, cx| thread.send(message, cx))?;
// Handle response stream and forward to session.acp_thread
while let Some(result) = response_stream.next().await {

View File

@@ -7,7 +7,7 @@ use gpui::{App, Entity, Task};
use project::Project;
use prompt_store::PromptStore;
use crate::{templates::Templates, NativeAgent, NativeAgentConnection};
use crate::{NativeAgent, NativeAgentConnection, templates::Templates};
#[derive(Clone)]
pub struct NativeAgentServer;

View File

@@ -1,17 +1,19 @@
use super::*;
use acp_thread::AgentConnection;
use action_log::ActionLog;
use agent_client_protocol::{self as acp};
use anyhow::Result;
use assistant_tool::ActionLog;
use client::{Client, UserStore};
use fs::FakeFs;
use fs::{FakeFs, Fs};
use futures::channel::mpsc::UnboundedReceiver;
use gpui::{http_client::FakeHttpClient, AppContext, Entity, Task, TestAppContext};
use gpui::{
App, AppContext, Entity, Task, TestAppContext, UpdateGlobal, http_client::FakeHttpClient,
};
use indoc::indoc;
use language_model::{
fake_provider::FakeLanguageModel, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelRegistry, LanguageModelToolResult,
LanguageModelToolUse, MessageContent, Role, StopReason,
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId,
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
StopReason, fake_provider::FakeLanguageModel,
};
use project::Project;
use prompt_store::ProjectContext;
@@ -19,6 +21,7 @@ use reqwest_client::ReqwestClient;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt;
use std::{cell::RefCell, path::Path, rc::Rc, sync::Arc, time::Duration};
use util::path;
@@ -29,11 +32,11 @@ use test_tools::*;
#[gpui::test]
#[ignore = "can't run on CI yet"]
async fn test_echo(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
let events = thread
.update(cx, |thread, cx| {
thread.send(model.clone(), "Testing: Reply with 'Hello'", cx)
thread.send("Testing: Reply with 'Hello'", cx)
})
.collect()
.await;
@@ -49,12 +52,11 @@ async fn test_echo(cx: &mut TestAppContext) {
#[gpui::test]
#[ignore = "can't run on CI yet"]
async fn test_thinking(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await;
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4Thinking).await;
let events = thread
.update(cx, |thread, cx| {
thread.send(
model.clone(),
indoc! {"
Testing:
@@ -91,7 +93,7 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
project_context.borrow_mut().shell = "test-shell".into();
thread.update(cx, |thread, _| thread.add_tool(EchoTool));
thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx));
thread.update(cx, |thread, cx| thread.send("abc", cx));
cx.run_until_parked();
let mut pending_completions = fake_model.pending_completions();
assert_eq!(
@@ -121,14 +123,13 @@ async fn test_system_prompt(cx: &mut TestAppContext) {
#[gpui::test]
#[ignore = "can't run on CI yet"]
async fn test_basic_tool_calls(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
// Test a tool call that's likely to complete *before* streaming stops.
let events = thread
.update(cx, |thread, cx| {
thread.add_tool(EchoTool);
thread.send(
model.clone(),
"Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'.",
cx,
)
@@ -143,7 +144,6 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
thread.remove_tool(&AgentTool::name(&EchoTool));
thread.add_tool(DelayTool);
thread.send(
model.clone(),
"Now call the delay tool with 200ms. When the timer goes off, then you echo the output of the tool.",
cx,
)
@@ -152,31 +152,33 @@ async fn test_basic_tool_calls(cx: &mut TestAppContext) {
.await;
assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]);
thread.update(cx, |thread, _cx| {
assert!(thread
.messages()
.last()
.unwrap()
.content
.iter()
.any(|content| {
if let MessageContent::Text(text) = content {
text.contains("Ding")
} else {
false
}
}));
assert!(
thread
.messages()
.last()
.unwrap()
.content
.iter()
.any(|content| {
if let MessageContent::Text(text) = content {
text.contains("Ding")
} else {
false
}
})
);
});
}
#[gpui::test]
#[ignore = "can't run on CI yet"]
async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
// Test a tool call that's likely to complete *before* streaming stops.
let mut events = thread.update(cx, |thread, cx| {
thread.add_tool(WordListTool);
thread.send(model.clone(), "Test the word_list tool.", cx)
thread.send("Test the word_list tool.", cx)
});
let mut saw_partial_tool_use = false;
@@ -223,7 +225,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let mut events = thread.update(cx, |thread, cx| {
thread.add_tool(ToolRequiringPermission);
thread.send(model.clone(), "abc", cx)
thread.send("abc", cx)
});
cx.run_until_parked();
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -283,6 +285,63 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
})
]
);
// Simulate yet another tool call.
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_3".into(),
name: ToolRequiringPermission.name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
},
));
fake_model.end_last_completion_stream();
// Respond by always allowing tools.
let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
tool_call_auth_3
.response
.send(tool_call_auth_3.options[0].id.clone())
.unwrap();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
})]
);
// Simulate a final tool call, ensuring we don't trigger authorization.
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
id: "tool_id_4".into(),
name: ToolRequiringPermission.name().into(),
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
},
));
fake_model.end_last_completion_stream();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
let message = completion.messages.last().unwrap();
assert_eq!(
message.content,
vec![MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: "tool_id_4".into(),
tool_name: ToolRequiringPermission.name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
})]
);
}
#[gpui::test]
@@ -290,7 +349,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "abc", cx));
let mut events = thread.update(cx, |thread, cx| thread.send("abc", cx));
cx.run_until_parked();
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
LanguageModelToolUse {
@@ -336,7 +395,7 @@ async fn expect_tool_call_update_fields(
.unwrap();
match event {
AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => {
return update
return update;
}
event => {
panic!("Unexpected event {event:?}");
@@ -375,14 +434,13 @@ async fn next_tool_call_authorization(
#[gpui::test]
#[ignore = "can't run on CI yet"]
async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
// Test concurrent tool calls with different delay times
let events = thread
.update(cx, |thread, cx| {
thread.add_tool(DelayTool);
thread.send(
model.clone(),
"Call the delay tool twice in the same message. Once with 100ms. Once with 300ms. When both timers are complete, describe the outputs.",
cx,
)
@@ -414,13 +472,12 @@ async fn test_concurrent_tool_calls(cx: &mut TestAppContext) {
#[gpui::test]
#[ignore = "can't run on CI yet"]
async fn test_cancellation(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Sonnet4).await;
let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await;
let mut events = thread.update(cx, |thread, cx| {
thread.add_tool(InfiniteTool);
thread.add_tool(EchoTool);
thread.send(
model.clone(),
"Call the echo tool and then call the infinite tool, then explain their output",
cx,
)
@@ -466,7 +523,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
// Ensure we can still send a new message after cancellation.
let events = thread
.update(cx, |thread, cx| {
thread.send(model.clone(), "Testing: reply with 'Hello' then stop.", cx)
thread.send("Testing: reply with 'Hello' then stop.", cx)
})
.collect::<Vec<_>>()
.await;
@@ -484,7 +541,7 @@ async fn test_refusal(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
let events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Hello", cx));
let events = thread.update(cx, |thread, cx| thread.send("Hello", cx));
cx.run_until_parked();
thread.read_with(cx, |thread, _| {
assert_eq!(
@@ -648,7 +705,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool));
let fake_model = model.as_fake();
let mut events = thread.update(cx, |thread, cx| thread.send(model.clone(), "Think", cx));
let mut events = thread.update(cx, |thread, cx| thread.send("Think", cx));
cx.run_until_parked();
// Simulate streaming partial input.
@@ -776,13 +833,17 @@ impl TestModel {
async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
cx.executor().allow_parking();
let fs = FakeFs::new(cx.background_executor.clone());
cx.update(|cx| {
settings::init(cx);
watch_settings(fs.clone(), cx);
Project::init_settings(cx);
agent_settings::init(cx);
});
let templates = Templates::new();
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(path!("/test"), json!({})).await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
@@ -844,3 +905,26 @@ fn init_logger() {
env_logger::init();
}
}
fn watch_settings(fs: Arc<dyn Fs>, cx: &mut App) {
let fs = fs.clone();
cx.spawn({
async move |cx| {
let mut new_settings_content_rx = settings::watch_config_file(
cx.background_executor(),
fs,
paths::settings_file().clone(),
);
while let Some(new_settings_content) = new_settings_content_rx.next().await {
cx.update(|cx| {
SettingsStore::update_global(cx, |settings, cx| {
settings.set_user_settings(&new_settings_content, cx)
})
})
.ok();
}
}
})
.detach();
}

View File

@@ -110,9 +110,9 @@ impl AgentTool for ToolRequiringPermission {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<String>> {
let auth_check = event_stream.authorize("Authorize?".into());
let authorize = event_stream.authorize("Authorize?", cx);
cx.foreground_executor().spawn(async move {
auth_check.await?;
authorize.await?;
Ok("Allowed".to_string())
})
}

View File

@@ -1,10 +1,12 @@
use crate::{SystemPromptTemplate, Template, Templates};
use acp_thread::Diff;
use action_log::ActionLog;
use agent_client_protocol as acp;
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::{adapt_schema_to_format, ActionLog};
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
use cloud_llm_client::{CompletionIntent, CompletionMode};
use collections::HashMap;
use fs::Fs;
use futures::{
channel::{mpsc, oneshot},
stream::FuturesUnordered,
@@ -21,9 +23,10 @@ use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, future::Future, rc::Rc, sync::Arc};
use util::{markdown::MarkdownCodeBlock, ResultExt};
use std::{cell::RefCell, collections::BTreeMap, fmt::Write, rc::Rc, sync::Arc};
use util::{ResultExt, markdown::MarkdownCodeBlock};
#[derive(Debug, Clone)]
pub struct AgentMessage {
@@ -200,11 +203,11 @@ impl Thread {
/// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn.
pub fn send(
&mut self,
model: Arc<dyn LanguageModel>,
content: impl Into<MessageContent>,
cx: &mut Context<Self>,
) -> mpsc::UnboundedReceiver<Result<AgentResponseEvent, LanguageModelCompletionError>> {
let content = content.into();
let model = self.selected_model.clone();
log::info!("Thread::send called with model: {:?}", model.name());
log::debug!("Thread::send content: {:?}", content);
@@ -506,8 +509,9 @@ impl Thread {
}));
};
let fs = self.project.read(cx).fs().clone();
let tool_event_stream =
ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone());
ToolCallEventStream::new(&tool_use, tool.kind(), event_stream.clone(), Some(fs));
tool_event_stream.update_fields(acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
@@ -801,47 +805,6 @@ impl AgentResponseEventStream {
.ok();
}
fn authorize_tool_call(
&self,
id: &LanguageModelToolUseId,
title: String,
kind: acp::ToolKind,
input: serde_json::Value,
) -> impl use<> + Future<Output = Result<()>> {
let (response_tx, response_rx) = oneshot::channel();
self.0
.unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization(
ToolCallAuthorization {
tool_call: Self::initial_tool_call(id, title, kind, input),
options: vec![
acp::PermissionOption {
id: acp::PermissionOptionId("always_allow".into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
},
acp::PermissionOption {
id: acp::PermissionOptionId("allow".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: acp::PermissionOptionId("deny".into()),
name: "Deny".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
response: response_tx,
},
)))
.ok();
async move {
match response_rx.await?.0.as_ref() {
"allow" | "always_allow" => Ok(()),
_ => Err(anyhow!("Permission to run tool denied by user")),
}
}
}
fn send_tool_call(
&self,
id: &LanguageModelToolUseId,
@@ -893,18 +856,6 @@ impl AgentResponseEventStream {
.ok();
}
fn update_tool_call_diff(&self, tool_use_id: &LanguageModelToolUseId, diff: Entity<Diff>) {
self.0
.unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateDiff {
id: acp::ToolCallId(tool_use_id.to_string().into()),
diff,
}
.into(),
)))
.ok();
}
fn send_stop(&self, reason: StopReason) {
match reason {
StopReason::EndTurn => {
@@ -937,6 +888,7 @@ pub struct ToolCallEventStream {
kind: acp::ToolKind,
input: serde_json::Value,
stream: AgentResponseEventStream,
fs: Option<Arc<dyn Fs>>,
}
impl ToolCallEventStream {
@@ -955,6 +907,7 @@ impl ToolCallEventStream {
},
acp::ToolKind::Other,
AgentResponseEventStream(events_tx),
None,
);
(stream, ToolCallEventStreamReceiver(events_rx))
@@ -964,12 +917,14 @@ impl ToolCallEventStream {
tool_use: &LanguageModelToolUse,
kind: acp::ToolKind,
stream: AgentResponseEventStream,
fs: Option<Arc<dyn Fs>>,
) -> Self {
Self {
tool_use_id: tool_use.id.clone(),
kind,
input: tool_use.input.clone(),
stream,
fs,
}
}
@@ -978,17 +933,85 @@ impl ToolCallEventStream {
.update_tool_call_fields(&self.tool_use_id, fields);
}
pub fn update_diff(&self, diff: Entity<Diff>) {
self.stream.update_tool_call_diff(&self.tool_use_id, diff);
pub fn update_diff(&self, diff: Entity<acp_thread::Diff>) {
self.stream
.0
.unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateDiff {
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
diff,
}
.into(),
)))
.ok();
}
pub fn authorize(&self, title: String) -> impl use<> + Future<Output = Result<()>> {
self.stream.authorize_tool_call(
&self.tool_use_id,
title,
self.kind.clone(),
self.input.clone(),
)
pub fn update_terminal(&self, terminal: Entity<acp_thread::Terminal>) {
self.stream
.0
.unbounded_send(Ok(AgentResponseEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateTerminal {
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
terminal,
}
.into(),
)))
.ok();
}
pub fn authorize(&self, title: impl Into<String>, cx: &mut App) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));
}
let (response_tx, response_rx) = oneshot::channel();
self.stream
.0
.unbounded_send(Ok(AgentResponseEvent::ToolCallAuthorization(
ToolCallAuthorization {
tool_call: AgentResponseEventStream::initial_tool_call(
&self.tool_use_id,
title.into(),
self.kind.clone(),
self.input.clone(),
),
options: vec![
acp::PermissionOption {
id: acp::PermissionOptionId("always_allow".into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
},
acp::PermissionOption {
id: acp::PermissionOptionId("allow".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: acp::PermissionOptionId("deny".into()),
name: "Deny".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
response: response_tx,
},
)))
.ok();
let fs = self.fs.clone();
cx.spawn(async move |cx| match response_rx.await?.0.as_ref() {
"always_allow" => {
if let Some(fs) = fs.clone() {
cx.update(|cx| {
update_settings_file::<AgentSettings>(fs, cx, |settings, _| {
settings.set_always_allow_tool_actions(true);
});
})?;
}
Ok(())
}
"allow" => Ok(()),
_ => Err(anyhow!("Permission to run tool denied by user")),
})
}
}
@@ -999,7 +1022,7 @@ pub struct ToolCallEventStreamReceiver(
#[cfg(test)]
impl ToolCallEventStreamReceiver {
pub async fn expect_tool_authorization(&mut self) -> ToolCallAuthorization {
pub async fn expect_authorization(&mut self) -> ToolCallAuthorization {
let event = self.0.next().await;
if let Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth))) = event {
auth
@@ -1007,6 +1030,18 @@ impl ToolCallEventStreamReceiver {
panic!("Expected ToolCallAuthorization but got: {:?}", event);
}
}
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
let event = self.0.next().await;
if let Some(Ok(AgentResponseEvent::ToolCallUpdate(
acp_thread::ToolCallUpdate::UpdateTerminal(update),
))) = event
{
update.terminal
} else {
panic!("Expected terminal but got: {:?}", event);
}
}
}
#[cfg(test)]

View File

@@ -1,9 +1,33 @@
mod copy_path_tool;
mod create_directory_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_file_tool;
mod fetch_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 terminal_tool;
mod thinking_tool;
mod web_search_tool;
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 fetch_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 terminal_tool::*;
pub use thinking_tool::*;
pub use web_search_tool::*;

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,177 @@
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
)?;
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![output.clone().into()]),
..Default::default()
});
}
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 {
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![output.clone().into()]),
..Default::default()
});
Task::ready(Ok(output))
} else {
let text = "No errors or warnings found in the project.";
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![text.into()]),
..Default::default()
});
Task::ready(Ok(text.into()))
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::{AgentTool, Thread, ToolCallEventStream};
use acp_thread::Diff;
use agent_client_protocol as acp;
use anyhow::{anyhow, Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
use cloud_llm_client::CompletionIntent;
use collections::HashSet;
@@ -133,7 +133,7 @@ impl EditFileTool {
&self,
input: &EditFileToolInput,
event_stream: &ToolCallEventStream,
cx: &App,
cx: &mut App,
) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));
@@ -147,8 +147,9 @@ impl EditFileTool {
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{
return cx.foreground_executor().spawn(
event_stream.authorize(format!("{} (local settings)", input.display_description)),
return event_stream.authorize(
format!("{} (local settings)", input.display_description),
cx,
);
}
@@ -156,9 +157,9 @@ impl EditFileTool {
// so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
return cx.foreground_executor().spawn(
event_stream
.authorize(format!("{} (global settings)", input.display_description)),
return event_stream.authorize(
format!("{} (global settings)", input.display_description),
cx,
);
}
}
@@ -173,8 +174,7 @@ impl EditFileTool {
if project_path.is_some() {
Task::ready(Ok(()))
} else {
cx.foreground_executor()
.spawn(event_stream.authorize(input.display_description.clone()))
event_stream.authorize(&input.display_description, cx)
}
}
}
@@ -457,7 +457,7 @@ mod tests {
use crate::Templates;
use super::*;
use assistant_tool::ActionLog;
use action_log::ActionLog;
use client::TelemetrySettings;
use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal};
@@ -942,7 +942,7 @@ 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)");
// Test 2: Path outside project should require confirmation
@@ -959,7 +959,7 @@ 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");
// Test 3: Relative path without .zed should not require confirmation
@@ -992,7 +992,7 @@ mod tests {
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)");
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed
@@ -1088,7 +1088,7 @@ mod tests {
});
if should_confirm {
stream_rx.expect_tool_authorization().await;
stream_rx.expect_authorization().await;
} else {
auth.await.unwrap();
assert!(
@@ -1192,7 +1192,7 @@ mod tests {
});
if should_confirm {
stream_rx.expect_tool_authorization().await;
stream_rx.expect_authorization().await;
} else {
auth.await.unwrap();
assert!(
@@ -1276,7 +1276,7 @@ mod tests {
});
if should_confirm {
stream_rx.expect_tool_authorization().await;
stream_rx.expect_authorization().await;
} else {
auth.await.unwrap();
assert!(
@@ -1339,7 +1339,7 @@ mod tests {
)
});
stream_rx.expect_tool_authorization().await;
stream_rx.expect_authorization().await;
// Test outside path with different modes
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
@@ -1355,7 +1355,7 @@ mod tests {
)
});
stream_rx.expect_tool_authorization().await;
stream_rx.expect_authorization().await;
// Test normal path with different modes
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();

View File

@@ -0,0 +1,161 @@
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");
}
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![text.clone().into()]),
..Default::default()
});
Ok(text)
})
}
}

View File

@@ -1,6 +1,6 @@
use crate::{AgentTool, ToolCallEventStream};
use agent_client_protocol as acp;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use gpui::{App, AppContext, Entity, SharedString, Task};
use language_model::LanguageModelToolResultContent;
use project::Project;

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,66 @@
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(),
};
let content = format!("The current datetime is {now}.");
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![content.clone().into()]),
..Default::default()
});
Task::ready(Ok(content))
}
}

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 action_log::ActionLog;
use agent_client_protocol::{self as acp};
use anyhow::{anyhow, Context, Result};
use assistant_tool::{outline, ActionLog};
use gpui::{Entity, Task};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::outline;
use gpui::{App, Entity, SharedString, Task};
use indoc::formatdoc;
use language::{Anchor, Point};
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 serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use ui::{App, SharedString};
use crate::{AgentTool, ToolCallEventStream};
@@ -270,7 +270,7 @@ impl AgentTool for ReadFileTool {
mod test {
use super::*;
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 serde_json::json;
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,105 @@
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::LanguageModelToolResultContent;
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()
}
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

@@ -442,10 +442,6 @@ impl Settings for AgentSettings {
&mut settings.inline_alternatives,
value.inline_alternatives.clone(),
);
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
);
merge(
&mut settings.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)
}

View File

@@ -17,6 +17,7 @@ test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true
agent.workspace = true
agent2.workspace = true

View File

@@ -1,17 +1,13 @@
use acp_thread::{
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use audio::{Audio, Sound};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
use std::process::ExitStatus;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use agent_client_protocol as acp;
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use collections::{HashMap, HashSet};
use editor::{
@@ -32,20 +28,20 @@ use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
use settings::{Settings as _, SettingsStore};
use std::{
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
time::Duration,
};
use terminal_view::TerminalView;
use text::{Anchor, BufferSnapshot};
use theme::ThemeSettings;
use ui::{
Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
};
use util::ResultExt;
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
use ::acp_thread::{
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk,
LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
};
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
@@ -63,6 +59,7 @@ pub struct AcpThreadView {
project: Entity<Project>,
thread_state: ThreadState,
diff_editors: HashMap<EntityId, Entity<Editor>>,
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
message_editor: Entity<Editor>,
message_set_from_history: Option<BufferSnapshot>,
_message_editor_subscription: Subscription,
@@ -78,6 +75,7 @@ pub struct AcpThreadView {
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
terminal_expanded: bool,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 1],
@@ -193,6 +191,7 @@ impl AcpThreadView {
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
diff_editors: Default::default(),
terminal_views: Default::default(),
list_state: list_state.clone(),
scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()),
last_error: None,
@@ -202,6 +201,7 @@ impl AcpThreadView {
edits_expanded: false,
plan_expanded: false,
editor_expanded: false,
terminal_expanded: true,
message_history,
_subscriptions: [subscription],
_cancel_task: None,
@@ -410,7 +410,7 @@ impl AcpThreadView {
}
if ix < text.len() {
let last_chunk = text[ix..].trim();
let last_chunk = text[ix..].trim_end();
if !last_chunk.is_empty() {
chunks.push(last_chunk.into());
}
@@ -676,6 +676,16 @@ impl AcpThreadView {
entry_ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.sync_diff_multibuffers(entry_ix, window, cx);
self.sync_terminals(entry_ix, window, cx);
}
fn sync_diff_multibuffers(
&mut self,
entry_ix: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
return;
@@ -739,6 +749,50 @@ impl AcpThreadView {
)
}
fn sync_terminals(&mut self, entry_ix: usize, window: &mut Window, cx: &mut Context<Self>) {
let Some(terminals) = self.entry_terminals(entry_ix, cx) else {
return;
};
let terminals = terminals.collect::<Vec<_>>();
for terminal in terminals {
if self.terminal_views.contains_key(&terminal.entity_id()) {
return;
}
let terminal_view = cx.new(|cx| {
let mut view = TerminalView::new(
terminal.read(cx).inner().clone(),
self.workspace.clone(),
None,
self.project.downgrade(),
window,
cx,
);
view.set_embedded_mode(Some(1000), cx);
view
});
let entity_id = terminal.entity_id();
cx.observe_release(&terminal, move |this, _, _| {
this.terminal_views.remove(&entity_id);
})
.detach();
self.terminal_views.insert(entity_id, terminal_view);
}
}
fn entry_terminals(
&self,
entry_ix: usize,
cx: &App,
) -> Option<impl Iterator<Item = Entity<acp_thread::Terminal>>> {
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
Some(entry.terminals().map(|terminal| terminal.clone()))
}
fn authenticate(
&mut self,
method: acp::AuthMethodId,
@@ -862,17 +916,26 @@ impl AcpThreadView {
.child(message_body)
.into_any()
}
AgentThreadEntry::ToolCall(tool_call) => div()
.w_full()
.py_1p5()
.px_5()
.child(self.render_tool_call(index, tool_call, window, cx))
.into_any(),
AgentThreadEntry::ToolCall(tool_call) => {
let has_terminals = tool_call.terminals().next().is_some();
div().w_full().py_1p5().px_5().map(|this| {
if has_terminals {
this.children(tool_call.terminals().map(|terminal| {
self.render_terminal_tool_call(terminal, tool_call, window, cx)
}))
} else {
this.child(self.render_tool_call(index, tool_call, window, cx))
}
})
}
.into_any(),
};
let Some(thread) = self.thread() else {
return primary;
};
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
if index == total_entries - 1 && !is_generating {
v_flex()
@@ -1101,19 +1164,27 @@ impl AcpThreadView {
),
};
let needs_confirmation = match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { .. } => true,
_ => tool_call
.content
.iter()
.any(|content| matches!(content, ToolCallContent::Diff { .. })),
};
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
);
let is_edit = matches!(tool_call.kind, acp::ToolKind::Edit);
let has_diff = tool_call
.content
.iter()
.any(|content| matches!(content, ToolCallContent::Diff { .. }));
let has_nonempty_diff = tool_call.content.iter().any(|content| match content {
ToolCallContent::Diff(diff) => diff.read(cx).has_revealed_range(cx),
_ => false,
});
let is_collapsible =
!tool_call.content.is_empty() && !needs_confirmation && !is_edit && !has_diff;
let is_open = tool_call.content.is_empty()
|| needs_confirmation
|| has_nonempty_diff
|| self.expanded_tool_calls.contains(&tool_call.id);
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
let gradient_color = cx.theme().colors().panel_background;
let gradient_overlay = {
let gradient_overlay = |color: Hsla| {
div()
.absolute()
.top_0()
@@ -1122,13 +1193,13 @@ impl AcpThreadView {
.h_full()
.bg(linear_gradient(
90.,
linear_color_stop(gradient_color, 1.),
linear_color_stop(gradient_color.opacity(0.2), 0.),
linear_color_stop(color, 1.),
linear_color_stop(color.opacity(0.2), 0.),
))
};
v_flex()
.when(needs_confirmation, |this| {
.when(needs_confirmation || is_edit || has_diff, |this| {
this.rounded_lg()
.border_1()
.border_color(self.tool_card_border_color(cx))
@@ -1142,7 +1213,7 @@ impl AcpThreadView {
.gap_1()
.justify_between()
.map(|this| {
if needs_confirmation {
if needs_confirmation || is_edit || has_diff {
this.pl_2()
.pr_1()
.py_1()
@@ -1219,13 +1290,23 @@ impl AcpThreadView {
.child(self.render_markdown(
tool_call.label.clone(),
default_markdown_style(
needs_confirmation,
needs_confirmation || is_edit || has_diff,
window,
cx,
),
)),
)
.child(gradient_overlay)
.map(|this| {
if needs_confirmation {
this.child(gradient_overlay(
self.tool_card_header_bg(cx),
))
} else {
this.child(gradient_overlay(
cx.theme().colors().panel_background,
))
}
})
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
@@ -1260,11 +1341,9 @@ impl AcpThreadView {
.children(tool_call.content.iter().map(|content| {
div()
.py_1p5()
.child(
self.render_tool_call_content(
content, window, cx,
),
)
.child(self.render_tool_call_content(
content, tool_call, window, cx,
))
.into_any_element()
}))
.child(self.render_permission_buttons(
@@ -1278,11 +1357,9 @@ impl AcpThreadView {
this.children(tool_call.content.iter().map(|content| {
div()
.py_1p5()
.child(
self.render_tool_call_content(
content, window, cx,
),
)
.child(self.render_tool_call_content(
content, tool_call, window, cx,
))
.into_any_element()
}))
}
@@ -1299,11 +1376,12 @@ impl AcpThreadView {
fn render_tool_call_content(
&self,
content: &ToolCallContent,
tool_call: &ToolCall,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
match content {
ToolCallContent::ContentBlock { content } => {
ToolCallContent::ContentBlock(content) => {
if let Some(md) = content.markdown() {
div()
.p_2()
@@ -1318,8 +1396,9 @@ impl AcpThreadView {
Empty.into_any_element()
}
}
ToolCallContent::Diff { diff, .. } => {
self.render_diff_editor(&diff.read(cx).multibuffer())
ToolCallContent::Diff(diff) => self.render_diff_editor(&diff.read(cx).multibuffer()),
ToolCallContent::Terminal(terminal) => {
self.render_terminal_tool_call(terminal, tool_call, window, cx)
}
}
}
@@ -1333,14 +1412,22 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> Div {
h_flex()
.p_1p5()
.py_1()
.pl_2()
.pr_1()
.gap_1()
.justify_end()
.justify_between()
.flex_wrap()
.when(!empty_content, |this| {
this.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
.children(options.iter().map(|option| {
.child(
div()
.min_w(rems_from_px(145.))
.child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
)
.child(h_flex().gap_0p5().children(options.iter().map(|option| {
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| match option.kind {
@@ -1373,7 +1460,7 @@ impl AcpThreadView {
);
}
}))
}))
})))
}
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
@@ -1389,6 +1476,245 @@ impl AcpThreadView {
.into_any()
}
fn render_terminal_tool_call(
&self,
terminal: &Entity<acp_thread::Terminal>,
tool_call: &ToolCall,
window: &Window,
cx: &Context<Self>,
) -> AnyElement {
let terminal_data = terminal.read(cx);
let working_dir = terminal_data.working_dir();
let command = terminal_data.command();
let started_at = terminal_data.started_at();
let tool_failed = matches!(
&tool_call.status,
ToolCallStatus::Rejected
| ToolCallStatus::Canceled
| ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Failed,
..
}
);
let output = terminal_data.output();
let command_finished = output.is_some();
let truncated_output = output.is_some_and(|output| output.was_content_truncated);
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
let command_failed = command_finished
&& output.is_some_and(|o| o.exit_status.is_none_or(|status| !status.success()));
let time_elapsed = if let Some(output) = output {
output.ended_at.duration_since(started_at)
} else {
started_at.elapsed()
};
let header_bg = cx
.theme()
.colors()
.element_background
.blend(cx.theme().colors().editor_foreground.opacity(0.025));
let border_color = cx.theme().colors().border.opacity(0.6);
let working_dir = working_dir
.as_ref()
.map(|path| format!("{}", path.display()))
.unwrap_or_else(|| "current directory".to_string());
let header = h_flex()
.id(SharedString::from(format!(
"terminal-tool-header-{}",
terminal.entity_id()
)))
.flex_none()
.gap_1()
.justify_between()
.rounded_t_md()
.child(
div()
.id(("command-target-path", terminal.entity_id()))
.w_full()
.max_w_full()
.overflow_x_scroll()
.child(
Label::new(working_dir)
.buffer_font(cx)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
)
.when(!command_finished, |header| {
header
.gap_1p5()
.child(
Button::new(
SharedString::from(format!("stop-terminal-{}", terminal.entity_id())),
"Stop",
)
.icon(IconName::Stop)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Small)
.icon_color(Color::Error)
.label_size(LabelSize::Small)
.tooltip(move |window, cx| {
Tooltip::with_meta(
"Stop This Command",
None,
"Also possible by placing your cursor inside the terminal and using regular terminal bindings.",
window,
cx,
)
})
.on_click({
let terminal = terminal.clone();
cx.listener(move |_this, _event, _window, cx| {
let inner_terminal = terminal.read(cx).inner().clone();
inner_terminal.update(cx, |inner_terminal, _cx| {
inner_terminal.kill_active_task();
});
})
}),
)
.child(Divider::vertical())
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
)
})
.when(tool_failed || command_failed, |header| {
header.child(
div()
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
.child(
Icon::new(IconName::Close)
.size(IconSize::Small)
.color(Color::Error),
)
.when_some(output.and_then(|o| o.exit_status), |this, status| {
this.tooltip(Tooltip::text(format!(
"Exited with code {}",
status.code().unwrap_or(-1),
)))
}),
)
})
.when(truncated_output, |header| {
let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
"Output exceeded terminal max lines and was \
truncated, the model received the first 16 KB."
.to_string()
} else {
format!(
"Output is {} long—to avoid unexpected token usage, \
only 16 KB was sent back to the model.",
format_file_size(output.original_content_len as u64, true),
)
}
} else {
"Output was truncated".to_string()
};
header.child(
h_flex()
.id(("terminal-tool-truncated-label", terminal.entity_id()))
.gap_1()
.child(
Icon::new(IconName::Info)
.size(IconSize::XSmall)
.color(Color::Ignored),
)
.child(
Label::new("Truncated")
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.tooltip(Tooltip::text(tooltip)),
)
})
.when(time_elapsed > Duration::from_secs(10), |header| {
header.child(
Label::new(format!("({})", duration_alt_display(time_elapsed)))
.buffer_font(cx)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
})
.child(
Disclosure::new(
SharedString::from(format!(
"terminal-tool-disclosure-{}",
terminal.entity_id()
)),
self.terminal_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.terminal_expanded = !this.terminal_expanded;
})),
);
let show_output =
self.terminal_expanded && self.terminal_views.contains_key(&terminal.entity_id());
v_flex()
.mb_2()
.border_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.rounded_lg()
.overflow_hidden()
.child(
v_flex()
.p_2()
.gap_0p5()
.bg(header_bg)
.text_xs()
.child(header)
.child(
MarkdownElement::new(
command.clone(),
terminal_command_markdown_style(window, cx),
)
.code_block_renderer(
markdown::CodeBlockRenderer::Default {
copy_button: false,
copy_button_on_hover: true,
border: false,
},
),
),
)
.when(show_output, |this| {
let terminal_view = self.terminal_views.get(&terminal.entity_id()).unwrap();
this.child(
div()
.pt_2()
.border_t_1()
.when(tool_failed || command_failed, |card| card.border_dashed())
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.text_ui_sm(cx)
.child(terminal_view.clone()),
)
})
.into_any()
}
fn render_agent_logo(&self) -> AnyElement {
Icon::new(self.agent.logo())
.color(Color::Muted)
@@ -2955,6 +3281,18 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
}
}
fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let default_md_style = default_markdown_style(true, window, cx);
MarkdownStyle {
base_text_style: TextStyle {
..default_md_style.base_text_style
},
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use agent_client_protocol::SessionId;

View File

@@ -1,9 +1,9 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
use acp_thread::{AcpThread, AcpThreadEvent};
use action_log::ActionLog;
use agent::{Thread, ThreadEvent, ThreadSummary};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::ActionLog;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{

View File

@@ -12,12 +12,10 @@ workspace = true
path = "src/assistant_tool.rs"
[dependencies]
action_log.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
clock.workspace = true
collections.workspace = true
derive_more.workspace = true
futures.workspace = true
gpui.workspace = true
icons.workspace = true
language.workspace = true
@@ -30,7 +28,6 @@ serde.workspace = true
serde_json.workspace = true
text.workspace = true
util.workspace = true
watch.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -1,4 +1,3 @@
mod action_log;
pub mod outline;
mod tool_registry;
mod tool_schema;
@@ -10,6 +9,7 @@ use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use action_log::ActionLog;
use anyhow::Result;
use gpui::AnyElement;
use gpui::AnyWindowHandle;
@@ -25,7 +25,6 @@ use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
pub use crate::action_log::*;
pub use crate::tool_registry::*;
pub use crate::tool_schema::*;
pub use crate::tool_working_set::*;

View File

@@ -1,4 +1,4 @@
use crate::ActionLog;
use action_log::ActionLog;
use anyhow::{Context as _, Result};
use gpui::{AsyncApp, Entity};
use language::{OutlineItem, ParseStatus};

View File

@@ -15,6 +15,7 @@ path = "src/assistant_tools.rs"
eval = []
[dependencies]
action_log.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModel;

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
@@ -85,7 +86,7 @@ impl Tool for DiagnosticsTool {
input: serde_json::Value,
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
@@ -158,10 +159,6 @@ impl Tool for DiagnosticsTool {
}
}
action_log.update(cx, |action_log, _cx| {
action_log.checked_project_diagnostics();
});
if has_diagnostics {
Task::ready(Ok(output.into())).into()
} else {

View File

@@ -5,8 +5,8 @@ mod evals;
mod streaming_fuzzy_matcher;
use crate::{Template, Templates};
use action_log::ActionLog;
use anyhow::Result;
use assistant_tool::ActionLog;
use cloud_llm_client::CompletionIntent;
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
pub use edit_parser::EditFormat;

View File

@@ -1,10 +1,11 @@
use std::sync::OnceLock;
use regex::Regex;
use smallvec::SmallVec;
use std::cell::LazyCell;
use util::debug_panic;
const START_MARKER: LazyCell<Regex> = LazyCell::new(|| Regex::new(r"\n?```\S*\n").unwrap());
const END_MARKER: LazyCell<Regex> = LazyCell::new(|| Regex::new(r"(^|\n)```\s*$").unwrap());
static START_MARKER: OnceLock<Regex> = OnceLock::new();
static END_MARKER: OnceLock<Regex> = OnceLock::new();
#[derive(Debug)]
pub enum CreateFileParserEvent {
@@ -43,10 +44,12 @@ impl CreateFileParser {
self.buffer.push_str(chunk);
let mut edit_events = SmallVec::new();
let start_marker_regex = START_MARKER.get_or_init(|| Regex::new(r"\n?```\S*\n").unwrap());
let end_marker_regex = END_MARKER.get_or_init(|| Regex::new(r"(^|\n)```\s*$").unwrap());
loop {
match &mut self.state {
ParserState::Pending => {
if let Some(m) = START_MARKER.find(&self.buffer) {
if let Some(m) = start_marker_regex.find(&self.buffer) {
self.buffer.drain(..m.end());
self.state = ParserState::WithinText;
} else {
@@ -65,7 +68,7 @@ impl CreateFileParser {
break;
}
ParserState::Finishing => {
if let Some(m) = END_MARKER.find(&self.buffer) {
if let Some(m) = end_marker_regex.find(&self.buffer) {
self.buffer.drain(m.start()..);
}
if !self.buffer.is_empty() {

View File

@@ -4,11 +4,11 @@ use crate::{
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
use action_log::ActionLog;
use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput,
ToolUseStatus,
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};

View File

@@ -3,8 +3,9 @@ use std::sync::Arc;
use std::{borrow::Cow, cell::RefCell};
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use futures::AsyncReadExt as _;
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};

View File

@@ -1,7 +1,8 @@
use crate::{schema::json_schema_for, ui::ToolCallCardHeader};
use action_log::ActionLog;
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use editor::Editor;
use futures::channel::oneshot::{self, Receiver};

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use futures::StreamExt;
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{OffsetRangeExt, ParseStatus, Point};

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, WorktreeSettings};

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;

View File

@@ -1,8 +1,9 @@
use std::sync::Arc;
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::Result;
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;

View File

@@ -1,6 +1,7 @@
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use assistant_tool::{ToolResultContent, outline};
use gpui::{AnyWindowHandle, App, Entity, Task};
use project::{ImageItem, image_store};
@@ -286,7 +287,7 @@ impl Tool for ReadFileTool {
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline.
Alternatively, you can fall back to the `grep` tool (if available)
to search the file for specific content."
}

View File

@@ -2,9 +2,10 @@ use crate::{
schema::json_schema_for,
ui::{COLLAPSED_LINES, ToolOutputPreview},
};
use action_log::ActionLog;
use agent_settings;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,

View File

@@ -1,8 +1,9 @@
use std::sync::Arc;
use crate::schema::json_schema_for;
use action_log::ActionLog;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use assistant_tool::{Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;

View File

@@ -2,9 +2,10 @@ use std::{sync::Arc, time::Duration};
use crate::schema::json_schema_for;
use crate::ui::ToolCallCardHeader;
use action_log::ActionLog;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use cloud_llm_client::{WebSearchResponse, WebSearchResult};
use futures::{Future, FutureExt, TryFutureExt};

View File

@@ -263,12 +263,12 @@ pub struct WebSearchBody {
pub query: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WebSearchResponse {
pub results: Vec<WebSearchResult>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct WebSearchResult {
pub title: String,
pub url: String,

View File

@@ -10,6 +10,7 @@ crash-handler.workspace = true
log.workspace = true
minidumper.workspace = true
paths.workspace = true
release_channel.workspace = true
smol.workspace = true
workspace-hack.workspace = true

View File

@@ -1,6 +1,7 @@
use crash_handler::CrashHandler;
use log::info;
use minidumper::{Client, LoopAction, MinidumpBinary};
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
use std::{
env,
@@ -9,7 +10,7 @@ use std::{
path::{Path, PathBuf},
process::{self, Command},
sync::{
OnceLock,
LazyLock, OnceLock,
atomic::{AtomicBool, Ordering},
},
thread,
@@ -22,7 +23,14 @@ pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false);
pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false);
const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60);
pub static GENERATE_MINIDUMPS: LazyLock<bool> = LazyLock::new(|| {
*RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok()
});
pub async fn init(id: String) {
if !*GENERATE_MINIDUMPS {
return;
}
let exe = env::current_exe().expect("unable to find ourselves");
let zed_pid = process::id();
// TODO: we should be able to get away with using 1 crash-handler process per machine,
@@ -138,6 +146,9 @@ impl minidumper::ServerHandler for CrashServer {
}
pub fn handle_panic() {
if !*GENERATE_MINIDUMPS {
return;
}
// wait 500ms for the crash handler process to start up
// if it's still not there just write panic info and no minidump
let retry_frequency = Duration::from_millis(100);

View File

@@ -338,8 +338,8 @@ impl DebugAdapter for CodeLldbDebugAdapter {
if command.is_none() {
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
let version_path =
if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
let version_path = match self.fetch_latest_adapter_version(delegate).await {
Ok(version) => {
adapters::download_adapter_from_github(
self.name(),
version.clone(),
@@ -351,10 +351,26 @@ impl DebugAdapter for CodeLldbDebugAdapter {
adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
remove_matching(&adapter_path, |entry| entry != version_path).await;
version_path
} else {
let mut paths = delegate.fs().read_dir(&adapter_path).await?;
paths.next().await.context("No adapter found")??
};
}
Err(e) => {
delegate.output_to_console("Unable to fetch latest version".to_string());
log::error!("Error fetching latest version of {}: {}", self.name(), e);
delegate.output_to_console(format!(
"Searching for adapters in: {}",
adapter_path.display()
));
let mut paths = delegate
.fs()
.read_dir(&adapter_path)
.await
.context("No cached adapter directory")?;
paths
.next()
.await
.context("No cached adapter found")?
.context("No cached adapter found")?
}
};
let adapter_dir = version_path.join("extension").join("adapter");
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
self.path_to_codelldb.set(path.clone()).ok();

View File

@@ -152,6 +152,9 @@ impl PythonDebugAdapter {
maybe!(async move {
let response = latest_release.filter(|response| response.status().is_success())?;
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
std::fs::create_dir_all(&download_dir).ok()?;
let mut output = String::new();
response
.into_body()

View File

@@ -36,7 +36,7 @@ use settings::Settings;
use std::sync::{Arc, LazyLock};
use task::{DebugScenario, TaskContext};
use tree_sitter::{Query, StreamingIterator as _};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tab, Tooltip, prelude::*};
use util::{ResultExt, debug_panic, maybe};
use workspace::SplitDirection;
use workspace::item::SaveOptions;
@@ -642,12 +642,14 @@ impl DebugPanel {
}
})
};
let documentation_button = || {
IconButton::new("debug-open-documentation", IconName::CircleHelp)
.icon_size(IconSize::Small)
.on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
.tooltip(Tooltip::text("Open Documentation"))
};
let logs_button = || {
IconButton::new("debug-open-logs", IconName::Notepad)
.icon_size(IconSize::Small)
@@ -658,16 +660,18 @@ impl DebugPanel {
};
Some(
div.border_b_1()
.border_color(cx.theme().colors().border)
.p_1()
div.w_full()
.py_1()
.px_1p5()
.justify_between()
.w_full()
.border_b_1()
.border_color(cx.theme().colors().border)
.when(is_side, |this| this.gap_1())
.child(
h_flex()
.justify_between()
.child(
h_flex().gap_2().w_full().when_some(
h_flex().gap_1().w_full().when_some(
active_session
.as_ref()
.map(|session| session.read(cx).running_state()),
@@ -679,6 +683,7 @@ impl DebugPanel {
let capabilities = running_state.read(cx).capabilities(cx);
let supports_detach =
running_state.read(cx).session().read(cx).is_attached();
this.map(|this| {
if thread_status == ThreadStatus::Running {
this.child(
@@ -686,8 +691,7 @@ impl DebugPanel {
"debug-pause",
IconName::DebugPause,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
@@ -698,7 +702,7 @@ impl DebugPanel {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Pause program",
"Pause Program",
&Pause,
&focus_handle,
window,
@@ -713,8 +717,7 @@ impl DebugPanel {
"debug-continue",
IconName::DebugContinue,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| this.continue_thread(cx),
@@ -724,7 +727,7 @@ impl DebugPanel {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Continue program",
"Continue Program",
&Continue,
&focus_handle,
window,
@@ -737,8 +740,7 @@ impl DebugPanel {
})
.child(
IconButton::new("debug-step-over", IconName::ArrowRight)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
@@ -750,7 +752,7 @@ impl DebugPanel {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Step over",
"Step Over",
&StepOver,
&focus_handle,
window,
@@ -764,8 +766,7 @@ impl DebugPanel {
"debug-step-into",
IconName::ArrowDownRight,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
@@ -777,7 +778,7 @@ impl DebugPanel {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Step in",
"Step In",
&StepInto,
&focus_handle,
window,
@@ -789,7 +790,6 @@ impl DebugPanel {
.child(
IconButton::new("debug-step-out", IconName::ArrowUpRight)
.icon_size(IconSize::Small)
.shape(ui::IconButtonShape::Square)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
@@ -801,7 +801,7 @@ impl DebugPanel {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Step out",
"Step Out",
&StepOut,
&focus_handle,
window,
@@ -813,7 +813,7 @@ impl DebugPanel {
.child(Divider::vertical())
.child(
IconButton::new("debug-restart", IconName::RotateCcw)
.icon_size(IconSize::XSmall)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, window, cx| {
@@ -835,7 +835,7 @@ impl DebugPanel {
)
.child(
IconButton::new("debug-stop", IconName::Power)
.icon_size(IconSize::XSmall)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, _window, cx| {
@@ -890,7 +890,7 @@ impl DebugPanel {
thread_status != ThreadStatus::Stopped
&& thread_status != ThreadStatus::Running,
)
.icon_size(IconSize::XSmall)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
&running_state,
|this, _, _, cx| {
@@ -915,7 +915,6 @@ impl DebugPanel {
},
),
)
.justify_around()
.when(is_side, |this| {
this.child(new_session_button())
.child(logs_button())
@@ -924,7 +923,7 @@ impl DebugPanel {
)
.child(
h_flex()
.gap_2()
.gap_0p5()
.when(is_side, |this| this.justify_between())
.child(
h_flex().when_some(
@@ -954,12 +953,15 @@ impl DebugPanel {
)
})
})
.when(!is_side, |this| this.gap_2().child(Divider::vertical()))
.when(!is_side, |this| {
this.gap_0p5().child(Divider::vertical())
})
},
),
)
.child(
h_flex()
.gap_0p5()
.children(self.render_session_menu(
self.active_session(),
self.running_state(cx),
@@ -1702,6 +1704,7 @@ impl Render for DebugPanel {
this.child(active_session)
} else {
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
let welcome_experience = v_flex()
.when_else(
docked_to_bottom,
@@ -1767,54 +1770,58 @@ impl Render for DebugPanel {
);
}),
);
let breakpoint_list =
v_flex()
.group("base-breakpoint-list")
.items_start()
.when_else(
docked_to_bottom,
|this| this.min_w_1_3().h_full(),
|this| this.w_full().h_2_3(),
)
.p_1()
.child(
h_flex()
.pl_1()
.w_full()
.justify_between()
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(h_flex().visible_on_hover("base-breakpoint-list").child(
let breakpoint_list = v_flex()
.group("base-breakpoint-list")
.when_else(
docked_to_bottom,
|this| this.min_w_1_3().h_full(),
|this| this.size_full().h_2_3(),
)
.child(
h_flex()
.track_focus(&self.breakpoint_list.focus_handle(cx))
.h(Tab::container_height(cx))
.p_1p5()
.w_full()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(
h_flex().visible_on_hover("base-breakpoint-list").child(
self.breakpoint_list.read(cx).render_control_strip(),
))
.track_focus(&self.breakpoint_list.focus_handle(cx)),
)
.child(Divider::horizontal())
.child(self.breakpoint_list.clone());
),
),
)
.child(self.breakpoint_list.clone());
this.child(
v_flex()
.h_full()
.size_full()
.gap_1()
.items_center()
.justify_center()
.child(
div()
.when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
.size_full()
.map(|this| {
if docked_to_bottom {
this.items_start()
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
.child(Divider::vertical())
} else {
this.items_end()
.child(welcome_experience)
.child(Divider::horizontal())
.child(breakpoint_list)
}
}),
),
.map(|this| {
if docked_to_bottom {
this.child(
h_flex()
.size_full()
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
.child(Divider::vertical()),
)
} else {
this.child(
v_flex()
.size_full()
.child(welcome_experience)
.child(Divider::horizontal())
.child(breakpoint_list),
)
}
}),
)
}
})

View File

@@ -48,10 +48,8 @@ use task::{
};
use terminal_view::TerminalView;
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, FluentBuilder,
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon as _,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Tab, Tooltip,
VisibleOnHover, VisualContext, Window, div, h_flex, v_flex,
FluentBuilder, IntoElement, Render, StatefulInteractiveElement, Tab, Tooltip, VisibleOnHover,
VisualContext, prelude::*,
};
use util::ResultExt;
use variable_list::VariableList;
@@ -419,13 +417,14 @@ pub(crate) fn new_debugger_pane(
.map_or(false, |item| item.read(cx).hovered);
h_flex()
.group(pane_group_id.clone())
.justify_between()
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.px_2()
.border_color(cx.theme().colors().border)
.track_focus(&focus_handle)
.group(pane_group_id.clone())
.pl_1p5()
.pr_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().tab_bar_background)
.on_action(|_: &menu::Cancel, window, cx| {
if cx.stop_active_drag(window) {
return;
@@ -514,6 +513,7 @@ pub(crate) fn new_debugger_pane(
)
.child({
let zoomed = pane.is_zoomed();
h_flex()
.visible_on_hover(pane_group_id)
.when(is_hovered, |this| this.visible())
@@ -537,7 +537,7 @@ pub(crate) fn new_debugger_pane(
IconName::Maximize
},
)
.icon_size(IconSize::XSmall)
.icon_size(IconSize::Small)
.on_click(cx.listener(move |pane, _, _, cx| {
let is_zoomed = pane.is_zoomed();
pane.set_zoomed(!is_zoomed, cx);
@@ -592,10 +592,11 @@ impl DebugTerminal {
}
impl gpui::Render for DebugTerminal {
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.track_focus(&self.focus_handle)
.size_full()
.bg(cx.theme().colors().editor_background)
.children(self.terminal.clone())
}
}

View File

@@ -23,11 +23,8 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, InteractiveElement,
IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, Render, RenderOnce,
Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, Toggleable,
Tooltip, Window, div, h_flex, px, v_flex,
Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar,
ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*,
};
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
@@ -569,6 +566,7 @@ impl BreakpointList {
.map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
.unwrap_or_else(SupportedBreakpointProperties::empty);
let strip_mode = self.strip_mode;
uniform_list(
"breakpoint-list",
self.breakpoints.len(),
@@ -591,7 +589,7 @@ impl BreakpointList {
}),
)
.track_scroll(self.scroll_handle.clone())
.flex_grow()
.flex_1()
}
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
@@ -630,6 +628,7 @@ impl BreakpointList {
pub(crate) fn render_control_strip(&self) -> AnyElement {
let selection_kind = self.selection_kind();
let focus_handle = self.focus_handle.clone();
let remove_breakpoint_tooltip = selection_kind.map(|(kind, _)| match kind {
SelectedBreakpointKind::Source => "Remove breakpoint from a breakpoint list",
SelectedBreakpointKind::Exception => {
@@ -637,6 +636,7 @@ impl BreakpointList {
}
SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list",
});
let toggle_label = selection_kind.map(|(_, is_enabled)| {
if is_enabled {
(
@@ -649,13 +649,12 @@ impl BreakpointList {
});
h_flex()
.gap_2()
.child(
IconButton::new(
"disable-breakpoint-breakpoint-list",
IconName::DebugDisabledBreakpoint,
)
.icon_size(IconSize::XSmall)
.icon_size(IconSize::Small)
.when_some(toggle_label, |this, (label, meta)| {
this.tooltip({
let focus_handle = focus_handle.clone();
@@ -681,9 +680,8 @@ impl BreakpointList {
}),
)
.child(
IconButton::new("remove-breakpoint-breakpoint-list", IconName::Close)
.icon_size(IconSize::XSmall)
.icon_color(ui::Color::Error)
IconButton::new("remove-breakpoint-breakpoint-list", IconName::Trash)
.icon_size(IconSize::Small)
.when_some(remove_breakpoint_tooltip, |this, tooltip| {
this.tooltip({
let focus_handle = focus_handle.clone();
@@ -710,7 +708,6 @@ impl BreakpointList {
}
}),
)
.mr_2()
.into_any_element()
}
}
@@ -791,6 +788,7 @@ impl Render for BreakpointList {
.chain(data_breakpoints)
.chain(exception_breakpoints),
);
v_flex()
.id("breakpoint-list")
.key_context("BreakpointList")
@@ -806,35 +804,33 @@ impl Render for BreakpointList {
.on_action(cx.listener(Self::next_breakpoint_property))
.on_action(cx.listener(Self::previous_breakpoint_property))
.size_full()
.m_0p5()
.child(
v_flex()
.size_full()
.child(self.render_list(cx))
.child(self.render_vertical_scrollbar(cx)),
)
.pt_1()
.child(self.render_list(cx))
.child(self.render_vertical_scrollbar(cx))
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal()).child(
h_flex()
// .w_full()
.m_0p5()
.p_0p5()
.border_1()
.rounded_sm()
.when(
self.input.focus_handle(cx).contains_focused(window, cx),
|this| {
let colors = cx.theme().colors();
let border = if self.input.read(cx).read_only(cx) {
colors.border_disabled
} else {
colors.border_focused
};
this.border_color(border)
},
)
.child(self.input.clone()),
)
this.child(Divider::horizontal().color(DividerColor::Border))
.child(
h_flex()
.p_1()
.rounded_sm()
.bg(cx.theme().colors().editor_background)
.border_1()
.when(
self.input.focus_handle(cx).contains_focused(window, cx),
|this| {
let colors = cx.theme().colors();
let border_color = if self.input.read(cx).read_only(cx) {
colors.border_disabled
} else {
colors.border_transparent
};
this.border_color(border_color)
},
)
.child(self.input.clone()),
)
})
}
}
@@ -865,12 +861,17 @@ impl LineBreakpoint {
let path = self.breakpoint.path.clone();
let row = self.breakpoint.row;
let is_enabled = self.breakpoint.state.is_enabled();
let indicator = div()
.id(SharedString::from(format!(
"breakpoint-ui-toggle-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.cursor_pointer()
.child(
Icon::new(icon_name)
.color(Color::Debugger)
.size(IconSize::XSmall),
)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -902,17 +903,14 @@ impl LineBreakpoint {
.ok();
}
})
.child(
Icon::new(icon_name)
.color(Color::Debugger)
.size(IconSize::XSmall),
)
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.toggle_state(is_selected)
.inset(true)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
@@ -922,23 +920,20 @@ impl LineBreakpoint {
.ok();
}
})
.start_slot(indicator)
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
.start_slot(indicator)
.child(
h_flex()
.w_full()
.mr_4()
.py_0p5()
.gap_1()
.min_h(px(26.))
.justify_between()
.id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.w_full()
.gap_1()
.min_h(rems_from_px(26.))
.justify_between()
.on_click({
let weak = weak.clone();
move |_, window, cx| {
@@ -949,9 +944,9 @@ impl LineBreakpoint {
.ok();
}
})
.cursor_pointer()
.child(
h_flex()
.id("label-container")
.gap_0p5()
.child(
Label::new(format!("{}:{}", self.name, self.line))
@@ -971,11 +966,13 @@ impl LineBreakpoint {
.line_height_style(ui::LineHeightStyle::UiLabel)
.truncate(),
)
})),
}))
.when_some(self.dir.as_ref(), |this, parent_dir| {
this.tooltip(Tooltip::text(format!(
"Worktree parent path: {parent_dir}"
)))
}),
)
.when_some(self.dir.as_ref(), |this, parent_dir| {
this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
})
.child(BreakpointOptionsStrip {
props,
breakpoint: BreakpointEntry {
@@ -988,15 +985,16 @@ impl LineBreakpoint {
index: ix,
}),
)
.toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
struct ExceptionBreakpoint {
id: String,
data: ExceptionBreakpointsFilter,
is_enabled: bool,
}
#[derive(Clone, Debug)]
struct DataBreakpoint(project::debugger::session::DataBreakpointState);
@@ -1017,17 +1015,24 @@ impl DataBreakpoint {
};
let is_enabled = self.0.is_enabled;
let id = self.0.dap.data_id.clone();
ListItem::new(SharedString::from(format!(
"data-breakpoint-ui-item-{}",
self.0.dap.data_id
)))
.rounded()
.toggle_state(is_selected)
.inset(true)
.start_slot(
div()
.id(SharedString::from(format!(
"data-breakpoint-ui-item-{}-click-handler",
self.0.dap.data_id
)))
.child(
Icon::new(IconName::Binary)
.color(color)
.size(IconSize::Small),
)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -1052,25 +1057,18 @@ impl DataBreakpoint {
})
.ok();
}
})
.cursor_pointer()
.child(
Icon::new(IconName::Binary)
.color(color)
.size(IconSize::Small),
),
}),
)
.child(
h_flex()
.w_full()
.mr_4()
.py_0p5()
.gap_1()
.min_h(rems_from_px(26.))
.justify_between()
.child(
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("data-breakpoint-label", ix))
.child(
@@ -1091,7 +1089,6 @@ impl DataBreakpoint {
index: ix,
}),
)
.toggle_state(is_selected)
}
}
@@ -1113,10 +1110,13 @@ impl ExceptionBreakpoint {
let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
let weak = list.clone();
ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}",
self.id
)))
.toggle_state(is_selected)
.inset(true)
.on_click({
let list = list.clone();
move |_, window, cx| {
@@ -1124,7 +1124,6 @@ impl ExceptionBreakpoint {
.ok();
}
})
.rounded()
.on_secondary_mouse_down(|_, _, cx| {
cx.stop_propagation();
})
@@ -1134,6 +1133,11 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.child(
Icon::new(IconName::Flame)
.color(color)
.size(IconSize::Small),
)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
@@ -1158,25 +1162,18 @@ impl ExceptionBreakpoint {
})
.ok();
}
})
.cursor_pointer()
.child(
Icon::new(IconName::Flame)
.color(color)
.size(IconSize::Small),
),
}),
)
.child(
h_flex()
.w_full()
.mr_4()
.py_0p5()
.gap_1()
.min_h(rems_from_px(26.))
.justify_between()
.child(
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("exception-breakpoint-label", ix))
.child(
@@ -1200,7 +1197,6 @@ impl ExceptionBreakpoint {
index: ix,
}),
)
.toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -1302,6 +1298,7 @@ impl BreakpointEntry {
}
}
}
bitflags::bitflags! {
#[derive(Clone, Copy)]
pub struct SupportedBreakpointProperties: u32 {
@@ -1360,6 +1357,7 @@ impl BreakpointOptionsStrip {
fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
self.is_selected && self.strip_mode == Some(expected_mode)
}
fn on_click_callback(
&self,
mode: ActiveBreakpointStripMode,
@@ -1379,7 +1377,8 @@ impl BreakpointOptionsStrip {
.ok();
}
}
fn add_border(
fn add_focus_styles(
&self,
kind: ActiveBreakpointStripMode,
available: bool,
@@ -1388,22 +1387,25 @@ impl BreakpointOptionsStrip {
) -> impl Fn(Div) -> Div {
move |this: Div| {
// Avoid layout shifts in case there's no colored border
let this = this.border_2().rounded_sm();
let this = this.border_1().rounded_sm();
let color = cx.theme().colors();
if self.is_selected && self.strip_mode == Some(kind) {
let theme = cx.theme().colors();
if self.focus_handle.is_focused(window) {
this.border_color(theme.border_selected)
this.bg(color.editor_background)
.border_color(color.border_focused)
} else {
this.border_color(theme.border_disabled)
this.border_color(color.border)
}
} else if !available {
this.border_color(cx.theme().colors().border_disabled)
this.border_color(color.border_transparent)
} else {
this
}
}
}
}
impl RenderOnce for BreakpointOptionsStrip {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.breakpoint.id();
@@ -1426,73 +1428,117 @@ impl RenderOnce for BreakpointOptionsStrip {
};
let color_for_toggle = |is_enabled| {
if is_enabled {
ui::Color::Default
Color::Default
} else {
ui::Color::Muted
Color::Muted
}
};
h_flex()
.gap_1()
.gap_px()
.mr_3() // Space to avoid overlapping with the scrollbar
.child(
div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::Log,
supports_logs,
window,
cx,
))
.child(
IconButton::new(
SharedString::from(format!("{id}-log-toggle")),
IconName::Notepad,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
.icon_size(IconSize::Small)
.icon_color(color_for_toggle(has_logs))
.when(has_logs, |this| this.indicator(Indicator::dot().color(Color::Info)))
.disabled(!supports_logs)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Log))
.tooltip(|window, cx| {
Tooltip::with_meta(
"Set Log Message",
None,
"Set log message to display (instead of stopping) when a breakpoint is hit.",
window,
cx,
)
}),
)
.when(!has_logs && !self.is_selected, |this| this.invisible()),
)
.child(
div().map(self.add_border(
ActiveBreakpointStripMode::Condition,
supports_condition,
window, cx
))
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::Condition,
supports_condition,
window,
cx,
))
.child(
IconButton::new(
SharedString::from(format!("{id}-condition-toggle")),
IconName::SplitAlt,
)
.icon_size(IconSize::XSmall)
.shape(ui::IconButtonShape::Square)
.style(style_for_toggle(
ActiveBreakpointStripMode::Condition,
has_condition
has_condition,
))
.icon_size(IconSize::Small)
.icon_color(color_for_toggle(has_condition))
.when(has_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
.disabled(!supports_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
.tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
.tooltip(|window, cx| {
Tooltip::with_meta(
"Set Condition",
None,
"Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.",
window,
cx,
)
}),
)
.when(!has_condition && !self.is_selected, |this| this.invisible()),
)
.child(
div().map(self.add_border(
ActiveBreakpointStripMode::HitCondition,
supports_hit_condition,window, cx
))
div()
.map(self.add_focus_styles(
ActiveBreakpointStripMode::HitCondition,
supports_hit_condition,
window,
cx,
))
.child(
IconButton::new(
SharedString::from(format!("{id}-hit-condition-toggle")),
IconName::ArrowDown10,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::HitCondition,
has_hit_condition,
))
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(color_for_toggle(has_hit_condition))
.when(has_hit_condition, |this| this.indicator(Indicator::dot().color(Color::Info)))
.disabled(!supports_hit_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition))
.tooltip(|window, cx| {
Tooltip::with_meta(
"Set Hit Condition",
None,
"Set expression that controls how many hits of the breakpoint are ignored.",
window,
cx,
)
}),
)
.when(!has_hit_condition && !self.is_selected, |this| {
this.invisible()

View File

@@ -367,7 +367,7 @@ impl Console {
.when_some(keybinding_target.clone(), |el, keybinding_target| {
el.context(keybinding_target.clone())
})
.action("Watch expression", WatchExpression.boxed_clone())
.action("Watch Expression", WatchExpression.boxed_clone())
}))
})
},
@@ -452,18 +452,22 @@ impl Render for Console {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let query_focus_handle = self.query_bar.focus_handle(cx);
self.update_output(window, cx);
v_flex()
.track_focus(&self.focus_handle)
.key_context("DebugConsole")
.on_action(cx.listener(Self::evaluate))
.on_action(cx.listener(Self::watch_expression))
.size_full()
.border_2()
.bg(cx.theme().colors().editor_background)
.child(self.render_console(cx))
.when(self.is_running(cx), |this| {
this.child(Divider::horizontal()).child(
h_flex()
.on_action(cx.listener(Self::previous_query))
.on_action(cx.listener(Self::next_query))
.p_1()
.gap_1()
.bg(cx.theme().colors().editor_background)
.child(self.render_query_bar(cx))
@@ -474,6 +478,9 @@ impl Render for Console {
.on_click(move |_, window, cx| {
window.dispatch_action(Box::new(Confirm), cx)
})
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.child(Label::new("Evaluate"))
.tooltip({
let query_focus_handle = query_focus_handle.clone();
@@ -486,10 +493,7 @@ impl Render for Console {
cx,
)
}
})
.layer(ui::ElevationIndex::ModalSurface)
.size(ui::ButtonSize::Compact)
.child(Label::new("Evaluate")),
}),
self.render_submit_menu(
ElementId::Name("split-button-right-confirm-button".into()),
Some(query_focus_handle.clone()),
@@ -499,7 +503,6 @@ impl Render for Console {
)),
)
})
.border_2()
}
}

View File

@@ -18,10 +18,8 @@ use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session:
use settings::Settings;
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, Color, Context, ContextMenu, Div, Divider, DropdownMenu, Element,
FluentBuilder, Icon, IconName, InteractiveElement, IntoElement, Label, LabelCommon,
ParentElement, Pixels, PopoverMenuHandle, Render, Scrollbar, ScrollbarState, SharedString,
StatefulInteractiveElement, Styled, TextSize, Tooltip, Window, div, h_flex, px, v_flex,
ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render,
Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*,
};
use workspace::Workspace;

View File

@@ -8,7 +8,7 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::io::{self, Read};
use std::process;
use std::sync::LazyLock;
use std::sync::{LazyLock, OnceLock};
use util::paths::PathExt;
static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
@@ -388,7 +388,7 @@ fn handle_postprocessing() -> Result<()> {
let meta_title = format!("{} | {}", page_title, meta_title);
zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
let contents = contents.replace("#description#", meta_description);
let contents = TITLE_REGEX
let contents = title_regex()
.replace(&contents, |_: &regex::Captures| {
format!("<title>{}</title>", meta_title)
})
@@ -404,10 +404,8 @@ fn handle_postprocessing() -> Result<()> {
) -> &'a std::path::Path {
&path.strip_prefix(&root).unwrap_or(&path)
}
const TITLE_REGEX: std::cell::LazyCell<Regex> =
std::cell::LazyCell::new(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap());
fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
let title_tag_contents = &TITLE_REGEX
let title_tag_contents = &title_regex()
.captures(&contents)
.with_context(|| format!("Failed to find title in {:?}", pretty_path))
.expect("Page has <title> element")[1];
@@ -420,3 +418,8 @@ fn handle_postprocessing() -> Result<()> {
title
}
}
fn title_regex() -> &'static Regex {
static TITLE_REGEX: OnceLock<Regex> = OnceLock::new();
TITLE_REGEX.get_or_init(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap())
}

14
crates/eval/build.rs Normal file
View File

@@ -0,0 +1,14 @@
fn main() {
let cargo_toml =
std::fs::read_to_string("../zed/Cargo.toml").expect("Failed to read crates/zed/Cargo.toml");
let version = cargo_toml
.lines()
.find(|line| line.starts_with("version = "))
.expect("Version not found in crates/zed/Cargo.toml")
.split('=')
.nth(1)
.expect("Invalid version format")
.trim()
.trim_matches('"');
println!("cargo:rustc-env=ZED_PKG_VERSION={}", version);
}

View File

@@ -337,7 +337,7 @@ pub struct AgentAppState {
}
pub fn init(cx: &mut App) -> Arc<AgentAppState> {
let app_version = AppVersion::global(cx);
let app_version = AppVersion::load(env!("ZED_PKG_VERSION"));
release_channel::init(app_version, cx);
gpui_tokio::init(cx);
@@ -350,7 +350,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
// Set User-Agent so we can download language servers from GitHub
let user_agent = format!(
"Zed/{} ({}; {})",
"Zed Agent Eval/{} ({}; {})",
app_version,
std::env::consts::OS,
std::env::consts::ARCH

View File

@@ -17,7 +17,7 @@ use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity,
Window, actions,
Window, actions, rems,
};
use open_path_prompt::OpenPathPrompt;
use picker::{Picker, PickerDelegate};
@@ -350,7 +350,7 @@ impl FileFinder {
pub fn modal_max_width(width_setting: Option<FileFinderWidth>, window: &mut Window) -> Pixels {
let window_width = window.viewport_size().width;
let small_width = Pixels(545.);
let small_width = rems(34.).to_pixels(window.rem_size());
match width_setting {
None | Some(FileFinderWidth::Small) => small_width,

View File

@@ -15,7 +15,6 @@ use ignore::gitignore::GitignoreBuilder;
use rope::Rope;
use smol::future::FutureExt as _;
use std::{path::PathBuf, sync::Arc};
use util::rel_path::RelPath;
#[derive(Clone)]
pub struct FakeGitRepository {
@@ -223,10 +222,7 @@ impl GitRepository for FakeGitRepository {
.read_file_sync(path)
.ok()
.map(|content| String::from_utf8(content).unwrap())?;
Some((
RepoPath::from(&RelPath::new(repo_path)),
(content, is_ignored),
))
Some((repo_path.into(), (content, is_ignored)))
})
.collect();
@@ -406,11 +402,11 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}

View File

@@ -12,7 +12,7 @@ use gpui::BackgroundExecutor;
use gpui::Global;
use gpui::ReadGlobal as _;
use std::borrow::Cow;
use util::command::new_std_command;
use util::command::{new_smol_command, new_std_command};
#[cfg(unix)]
use std::os::fd::{AsFd, AsRawFd};
@@ -134,6 +134,7 @@ pub trait Fs: Send + Sync {
fn home_dir(&self) -> Option<PathBuf>;
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>>;
fn git_init(&self, abs_work_directory: &Path, fallback_branch_name: String) -> Result<()>;
async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()>;
fn is_fake(&self) -> bool;
async fn is_case_sensitive(&self) -> Result<bool>;
@@ -839,6 +840,23 @@ impl Fs for RealFs {
Ok(())
}
async fn git_clone(&self, repo_url: &str, abs_work_directory: &Path) -> Result<()> {
let output = new_smol_command("git")
.current_dir(abs_work_directory)
.args(&["clone", repo_url])
.output()
.await?;
if !output.status.success() {
anyhow::bail!(
"git clone failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
fn is_fake(&self) -> bool {
false
}
@@ -2154,6 +2172,9 @@ impl Fs for FakeFs {
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());
if let Some(path) = path.parent() {
self.create_dir(path).await?;
}
self.write_file_internal(path, data.into_bytes(), true)?;
Ok(())
}
@@ -2352,6 +2373,10 @@ impl Fs for FakeFs {
smol::block_on(self.create_dir(&abs_work_directory_path.join(".git")))
}
async fn git_clone(&self, _repo_url: &str, _abs_work_directory: &Path) -> Result<()> {
anyhow::bail!("Git clone is not supported in fake Fs")
}
fn is_fake(&self) -> bool {
true
}

View File

@@ -1,5 +1,4 @@
use crate::commit::get_messages;
use crate::repository::RepoPath;
use crate::{GitRemote, Oid};
use anyhow::{Context as _, Result};
use collections::{HashMap, HashSet};
@@ -34,7 +33,7 @@ impl Blame {
pub async fn for_path(
git_binary: &Path,
working_directory: &Path,
path: &RepoPath,
path: &Path,
content: &Rope,
remote_url: Option<String>,
) -> Result<Self> {
@@ -67,7 +66,7 @@ const GIT_BLAME_NO_PATH: &str = "fatal: no such path";
async fn run_git_blame(
git_binary: &Path,
working_directory: &Path,
path: &RepoPath,
path: &Path,
contents: &Rope,
) -> Result<String> {
let mut child = util::command::new_smol_command(git_binary)

Binary file not shown.

View File

@@ -93,6 +93,8 @@ actions!(
Init,
/// Opens all modified files in the editor.
OpenModifiedFiles,
/// Clones a repository.
Clone,
]
);

View File

@@ -27,7 +27,6 @@ use std::{
use sum_tree::MapSeekTarget;
use thiserror::Error;
use util::command::{new_smol_command, new_std_command};
use util::rel_path::RelPath;
use util::{ResultExt, paths};
use uuid::Uuid;
@@ -400,9 +399,9 @@ pub trait GitRepository: Send + Sync {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
) -> BoxFuture<'_, Result<()>>;
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>>;
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>>;
fn push(
&self,
@@ -663,22 +662,14 @@ impl GitRepository for RealGitRepository {
for (path, status_code) in changes {
match status_code {
StatusCode::Modified => {
write!(&mut stdin, "{commit}:")?;
stdin.write_all(path.as_bytes())?;
stdin.write_all(b"\n")?;
write!(&mut stdin, "{parent_sha}:")?;
stdin.write_all(path.as_bytes())?;
stdin.write_all(b"\n")?;
writeln!(&mut stdin, "{commit}:{}", path.display())?;
writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
}
StatusCode::Added => {
write!(&mut stdin, "{commit}:")?;
stdin.write_all(path.as_bytes())?;
stdin.write_all(b"\n")?;
writeln!(&mut stdin, "{commit}:{}", path.display())?;
}
StatusCode::Deleted => {
write!(&mut stdin, "{parent_sha}:")?;
stdin.write_all(path.as_bytes())?;
stdin.write_all(b"\n")?;
writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
}
_ => continue,
}
@@ -774,7 +765,7 @@ impl GitRepository for RealGitRepository {
.current_dir(&working_directory?)
.envs(env.iter())
.args(["checkout", &commit, "--"])
.args(paths.iter().map(|path| path.to_unix_style()))
.args(paths.iter().map(|path| path.as_ref()))
.output()
.await?;
anyhow::ensure!(
@@ -796,14 +787,13 @@ impl GitRepository for RealGitRepository {
.spawn(async move {
fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
// This check is required because index.get_path() unwraps internally :(
// TODO: move this function to where we instantiate the repopaths
// check_path_to_repo_path_errors(path)?;
check_path_to_repo_path_errors(path)?;
let mut index = repo.index()?;
index.read(false)?;
const STAGE_NORMAL: i32 = 0;
let oid = match index.get_path(Path::new(&path.to_unix_style()), STAGE_NORMAL) {
let oid = match index.get_path(path, STAGE_NORMAL) {
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
_ => return Ok(None),
};
@@ -827,7 +817,7 @@ impl GitRepository for RealGitRepository {
.spawn(async move {
let repo = repo.lock();
let head = repo.head().ok()?.peel_to_tree().log_err()?;
let entry = head.get_path(Path::new(&path.as_os_str())).ok()?;
let entry = head.get_path(&path).ok()?;
if entry.filemode() == i32::from(git2::FileMode::Link) {
return None;
}
@@ -1194,7 +1184,7 @@ impl GitRepository for RealGitRepository {
.current_dir(&working_directory?)
.envs(env.iter())
.args(["reset", "--quiet", "--"])
.args(paths.iter().map(|p| p.to_unix_style()))
.args(paths.iter().map(|p| p.as_ref()))
.output()
.await?;
@@ -1213,7 +1203,7 @@ impl GitRepository for RealGitRepository {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -1223,7 +1213,7 @@ impl GitRepository for RealGitRepository {
.args(["stash", "push", "--quiet"])
.arg("--include-untracked");
cmd.args(paths.iter().map(|p| p.to_unix_style()));
cmd.args(paths.iter().map(|p| p.as_ref()));
let output = cmd.output().await?;
@@ -1237,7 +1227,7 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
self.executor
.spawn(async move {
@@ -1502,13 +1492,19 @@ impl GitRepository for RealGitRepository {
let mut excludes = exclude_files(git).await?;
git.run(&["add", "--all"]).await?;
let tree = git.run(&["write-tree"]).await?;
dbg!("added all files");
let tree = git.run(&["write-tree"]).await;
dbg!(&tree);
let tree = tree?;
let checkpoint_sha = if let Some(head_sha) = head_sha.as_deref() {
dbg!(&["git", "commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"]));
git.run(&["commit-tree", &tree, "-p", head_sha, "-m", "Checkpoint"])
.await?
} else {
dbg!(&["git", "commit-tree", &tree, "-m", "Checkpoint"]);
git.run(&["commit-tree", &tree, "-m", "Checkpoint"]).await?
};
dbg!(&checkpoint_sha);
excludes.restore_original().await?;
@@ -1561,6 +1557,8 @@ impl GitRepository for RealGitRepository {
left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint,
) -> BoxFuture<'_, Result<bool>> {
// todo! fail or short circuit
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
@@ -1569,6 +1567,11 @@ impl GitRepository for RealGitRepository {
.spawn(async move {
let working_directory = working_directory?;
let git = GitBinary::new(git_binary_path, working_directory, executor);
log::error!(
"git diff-tree --quiet {} {}",
left.commit_sha,
right.commit_sha
);
let result = git
.run(&[
"diff-tree",
@@ -1577,6 +1580,7 @@ impl GitRepository for RealGitRepository {
&right.commit_sha.to_string(),
])
.await;
dbg!(&result);
match result {
Ok(_) => Ok(true),
Err(error) => {
@@ -1662,7 +1666,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
OsString::from("-z"),
];
args.extend(path_prefixes.iter().map(|path_prefix| {
if path_prefix.0.as_ref() == RelPath::new("") {
if path_prefix.0.as_ref() == Path::new("") {
Path::new(".").into()
} else {
path_prefix.as_os_str().into()
@@ -1915,33 +1919,64 @@ async fn run_askpass_command(
}
pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
LazyLock::new(|| RepoPath(RelPath::new("").into()));
LazyLock::new(|| RepoPath(Path::new("").into()));
#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
pub struct RepoPath(pub Arc<RelPath>);
pub struct RepoPath(pub Arc<Path>);
impl RepoPath {
pub fn new(path: PathBuf) -> Self {
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path.into())
}
pub fn from_str(path: &str) -> Self {
RepoPath(RelPath::new(path).into())
let path = Path::new(path);
debug_assert!(path.is_relative(), "Repo paths must be relative");
RepoPath(path.into())
}
pub fn to_unix_style(&self) -> Cow<'_, OsStr> {
self.0.as_os_str()
#[cfg(target_os = "windows")]
{
use std::ffi::OsString;
let path = self.0.as_os_str().to_string_lossy().replace("\\", "/");
Cow::Owned(OsString::from(path))
}
#[cfg(not(target_os = "windows"))]
{
Cow::Borrowed(self.0.as_os_str())
}
}
}
impl From<&RelPath> for RepoPath {
fn from(value: &RelPath) -> Self {
RepoPath(value.into())
impl std::fmt::Display for RepoPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.to_string_lossy().fmt(f)
}
}
impl From<Arc<RelPath>> for RepoPath {
fn from(value: Arc<RelPath>) -> Self {
impl From<&Path> for RepoPath {
fn from(value: &Path) -> Self {
RepoPath::new(value.into())
}
}
impl From<Arc<Path>> for RepoPath {
fn from(value: Arc<Path>) -> Self {
RepoPath(value)
}
}
impl From<PathBuf> for RepoPath {
fn from(value: PathBuf) -> Self {
RepoPath::new(value)
}
}
impl From<&str> for RepoPath {
fn from(value: &str) -> Self {
Self::from_str(value)
@@ -1950,32 +1985,32 @@ impl From<&str> for RepoPath {
impl Default for RepoPath {
fn default() -> Self {
RepoPath(RelPath::new("").into())
RepoPath(Path::new("").into())
}
}
impl AsRef<RelPath> for RepoPath {
fn as_ref(&self) -> &RelPath {
impl AsRef<Path> for RepoPath {
fn as_ref(&self) -> &Path {
self.0.as_ref()
}
}
impl std::ops::Deref for RepoPath {
type Target = RelPath;
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Borrow<RelPath> for RepoPath {
fn borrow(&self) -> &RelPath {
impl Borrow<Path> for RepoPath {
fn borrow(&self) -> &Path {
self.0.as_ref()
}
}
#[derive(Debug)]
pub struct RepoPathDescendants<'a>(pub &'a RelPath);
pub struct RepoPathDescendants<'a>(pub &'a Path);
impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
@@ -2059,6 +2094,35 @@ fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
}))
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
match relative_file_path.components().next() {
None => anyhow::bail!("repo path should not be empty"),
Some(Component::Prefix(_)) => anyhow::bail!(
"repo path `{}` should be relative, not a windows prefix",
relative_file_path.to_string_lossy()
),
Some(Component::RootDir) => {
anyhow::bail!(
"repo path `{}` should be relative",
relative_file_path.to_string_lossy()
)
}
Some(Component::CurDir) => {
anyhow::bail!(
"repo path `{}` should not start with `.`",
relative_file_path.to_string_lossy()
)
}
Some(Component::ParentDir) => {
anyhow::bail!(
"repo path `{}` should not start with `..`",
relative_file_path.to_string_lossy()
)
}
_ => Ok(()),
}
}
fn checkpoint_author_envs() -> HashMap<String, String> {
HashMap::from_iter([
("GIT_AUTHOR_NAME".to_string(), "Zed".to_string()),

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