Compare commits

...

165 Commits

Author SHA1 Message Date
Michael Sloan
dc2879759c Progress towards scoping zeta edit history to projects 2025-09-02 00:26:50 -06:00
Michael Sloan
523c514669 Merge branch 'main' into zeta-record-recently-active-files-when-data-collection-is-enabled 2025-09-01 12:54:22 -06:00
localcc
d910feac1d Implement perceptual gamma / contrast correction (#37167)
Closes #36023 

This improves font rendering quality by doing perceptual gamma+contrast
correction which makes font edges look nicer and more legible.

A comparison image: (left is old, right is new)
<img width="1638" height="854" alt="Screenshot 2025-08-29 140015"
src="https://github.com/user-attachments/assets/85ca9818-0d55-4af0-a796-19e8cf9ed36b"
/>

This is most noticeable on smaller fonts / low-dpi displays

Release Notes:

- Improved font rendering quality
2025-09-01 20:07:45 +02:00
张小白
61175ab9cd windows: Don’t skip the typo check for the windows folder (#37314)
Try to narrow down the scope of typo checking


Release Notes:

- N/A
2025-09-01 15:26:25 +00:00
雷电梅
2790eb604a deepseek: Fix API URL (#33905)
Closes #33904 

Release Notes:

- Add support for custom API Urls for DeepSeek Provider

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-01 10:49:09 +02:00
张小白
acff65ed3f windows: Update documents about WSL (#37292)
Release Notes:

- N/A
2025-09-01 08:33:59 +00:00
Ivan Trubach
3315fd94d2 editor: Add an option to disable rounded corners for text selection (#36987)
Closes #19891

Similar to VSCode’s `editor.roundedSelection` option.

#### Before/after

<table>
<tr><th><th>Enabled (default)</th><th>Disabled</th>
<tr><td>Editor-based UIs<td><img width="268" height="58" alt="image"
src="https://github.com/user-attachments/assets/f58c6817-88fc-4cba-b2bc-f7eff58ec6e5"
/>
<img width="146" height="97" alt="image"
src="https://github.com/user-attachments/assets/0cd08afa-8243-4d4e-a5c6-9055f6834ecf"
/><td><img width="272" height="54" alt="image"
src="https://github.com/user-attachments/assets/286c8f53-1973-442e-8446-4f48e3feca30"
/>
<img width="133" height="90" alt="image"
src="https://github.com/user-attachments/assets/4aea2044-403c-47a5-bb6d-a88a0b65814e"
/></td>
<tr><td>Terminal<td><img width="287" height="84" alt="image"
src="https://github.com/user-attachments/assets/b1594f68-2ef6-4bdc-9030-e67d55a5bf99"
/><td><img width="289" height="79" alt="image"
src="https://github.com/user-attachments/assets/6d095d9d-b408-4440-a9f5-6a2af2b84b61"
/></td>
</table>

Release Notes:

- Added setting `rounded_selection` to disable rounded corners for text
selection.
2025-09-01 11:21:55 +03:00
Lukas Wirth
62083fe796 gpui: Do not render ligatures between different styled text runs (#37175)
Currently when we render text with differing styles adjacently we might
form a ligature between the text, causing the ligature forming
characters to take on one of the two styles. This can especially become
confusing when a ligature is formed between actual text and inlay hints.

Annoyingly, the only ways to prevent this with core text is to either
render each run separately, or to insert a zero-width non-joiner to
force core text to break the ligatures apart, as it otherwise will merge
subsequent font runs of the same fonts.

We currently do layouting on a per line basis and it is unlikely we want
to change that as it would incur a lot of complexity and annoyances to
merge things back into a line, so this goes with the other approach of
inserting ZWNJ characters instead.

Note that neither linux nor windows seem to currently render ligatures,
so this only concerns macOS rendering at the moment.

Release Notes:

- Fixed ligatures forming between real text and inlay hints on macOS
2025-09-01 09:49:52 +02:00
Gaauwe Rombouts
a852bcc094 Improve system window tabs visibility (#37244)
Follow up of https://github.com/zed-industries/zed/pull/33334

After chatting with @MrSubidubi we found out that he had an old defaults
setting (most likely from when he encountered a previous window tabbing
bug):
```
❯ defaults read dev.zed.Zed-Nightly
{
    NSNavPanelExpandedSizeForOpenMode = "{800, 448}";
    NSNavPanelExpandedSizeForSaveMode = "{800, 448}";
    NSNavPanelExpandedStateForSaveMode = 1;
    NSOSPLastRootDirectory = {length = 828, bytes = 0x626f6f6b 3c030000 00000410 30000000 ... dc010000 00000000 };
    "NSWindow Frame NSNavPanelAutosaveName" = "557 1726 800 448 -323 982 2560 1440 ";
    "NSWindowTabbingShoudShowTabBarKey-GPUIWindow-GPUIWindow-(null)-HT-FS" = 1;
}
```

> That suffix is AppKit’s fallback autosave name when no tabbing
identifier is set. It encodes the NSWindow subclass (GPUIWindow), plus
traits like HT (hidden titlebar) and FS (fullscreen).

Which explains why it only happened on the Nightly build, since each
bundle has it's own defaults. It also explains why the tabbar would
disappear when he activated the `use_system_window_tabs` setting,
because with that setting activated, the tabbing identifier becomes
"zed" (instead of the default one when omitted) for which he didn't have
the `NSWindowTabbingShoudShowTabBarKey` default.

The original implementation was perhaps a bit naive and relied fully on
macOS to determine if the tabbar should be shown. I've updated the code
to always hide the tabbar, if the setting is turned off and there is
only 1 tab entry.

While testing, I also noticed that the menu's like 'merge all windows'
wouldn't become active when the setting was turned on, only after a full
workspace reload. So I added a setting observer as well, to immediately
set the correct window properties to enable all the features without a
reload.

Release Notes:

- N/A
2025-08-31 18:24:00 -06:00
Peter Tripp
f290daf7ea docs: Improve Bedrock suggested IAM policy (#37278)
Closes https://github.com/zed-industries/zed/issues/37251

H/T: @brandon-fryslie

Release Notes:

- N/A
2025-08-31 20:08:17 -04:00
Peter Tripp
129bff8358 agent: Make it so delete_path tool needs user confirmation (#37191)
Closes https://github.com/zed-industries/zed/issues/37048

Release Notes:

- agent: Make delete_path tool require user confirmation by default
2025-08-31 19:52:43 -04:00
Umesh Yadav
c833f8905b language_models: Fix grok-code-fast-1 support for Copilot (#37116)
This PR fixes a deserialization issue in GitHub Copilot Chat that was
causing warnings when encountering xAI models from the GitHub Copilot
API and skipping the Grok model from model selector.

Release Notes:

- Fixed support for xAI models that are now available through GitHub
Copilot Chat.
2025-08-31 18:51:17 -04:00
tidely
d74384f6e2 anthropic: Remove logging when no credentials are available (#37276)
Removes excess log which got through on each start of Zed
```
ERROR [agent_ui::language_model_selector] Failed to authenticate provider: Anthropic: credentials not found
```

The `AnthropicLanguageModelProvider::api_key` method returned a
`anyhow::Result` which would convert
`AuthenticateError::CredentialsNotFound` into a generic error because of
the implicit `Into` when using the `?` operator. This would then get
converted into a `AuthenticateError::Other` later.

By specifying the error type as `AuthenticateError`, we remove this
implicit conversion and the log gets removed.

Release Notes:

- N/A
2025-09-01 00:42:57 +03:00
Jakub Konka
5abc398a0a nix: Update flake, remove legacy Darwin SDK usage (#37254)
`darwin.apple_sdk.frameworks` has been obsoleted and is no longer
required to be specified explicitly as per [Nixpkgs Reference
Manual](https://nixos.org/manual/nixpkgs/stable/#sec-darwin-legacy-frameworks).

@P1n3appl3 not sure what the process for updating Nix is, so lemme know
if this is desired/acceptable!

Release Notes:

- N/A
2025-08-31 14:09:09 -07:00
Peter Tripp
9c8c3966df linux: Support ctrl-insert in markdown previews (#37273)
Closes: https://github.com/zed-industries/zed/issues/37240

Release Notes:

- Added support for copying in Markdown preview using `ctrl-insert` on Linux/Windows
2025-08-31 19:57:24 +00:00
Finn Evers
e48be30266 vim: Fix NormalBefore with completions shown (#37272)
Follow-up to https://github.com/zed-industries/zed/pull/35985

The `!menu` is actually not needed and breaks other keybinds from that
context.

Release Notes:

- N/A
2025-08-31 18:39:26 +00:00
Kirill Bulatov
babc0c09f0 Add a "mandatory PR contents" section in the contribution docs (#37259)
The LLM part is inspired by (and paraphrased from)
https://github.com/ghostty-org/ghostty?tab=contributing-ov-file#ai-assistance-notice

Release Notes:

- N/A
2025-08-31 20:56:23 +03:00
Kirill Bulatov
39d41ed822 Add another entry to show how to hide the Sign In button from the interface (#37260)
Release Notes:

- N/A
2025-08-31 10:29:29 +00:00
Kirill Bulatov
b69ebbd7b7 Bump pnpm dependencies (#37258)
Takes care of
https://github.com/zed-industries/zed/security/dependabot/64

Release Notes:

- N/A
2025-08-31 10:19:12 +00:00
renovate[bot]
f348737e8c Update Rust crate tracing-subscriber to v0.3.20 [SECURITY] (#37195)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [tracing-subscriber](https://tokio.rs)
([source](https://redirect.github.com/tokio-rs/tracing)) | dependencies
| patch | `0.3.19` -> `0.3.20` |

### GitHub Vulnerability Alerts

####
[CVE-2025-58160](https://redirect.github.com/tokio-rs/tracing/security/advisories/GHSA-xwfj-jgwm-7wp5)

### Impact

Previous versions of tracing-subscriber were vulnerable to ANSI escape
sequence injection attacks. Untrusted user input containing ANSI escape
sequences could be injected into terminal output when logged,
potentially allowing attackers to:

- Manipulate terminal title bars
- Clear screens or modify terminal display
- Potentially mislead users through terminal manipulation

In isolation, impact is minimal, however security issues have been found
in terminal emulators that enabled an attacker to use ANSI escape
sequences via logs to exploit vulnerabilities in the terminal emulator.

### Patches

`tracing-subscriber` version 0.3.20 fixes this vulnerability by escaping
ANSI control characters in when writing events to destinations that may
be printed to the terminal.

### Workarounds

Avoid printing logs to terminal emulators without escaping ANSI control
sequences.

### References

https://www.packetlabs.net/posts/weaponizing-ansi-escape-sequences/

### Acknowledgments

We would like to thank [zefr0x](http://github.com/zefr0x) who
responsibly reported the issue at `security@tokio.rs`.

If you believe you have found a security vulnerability in any tokio-rs
project, please email us at `security@tokio.rs`.

---

### Release Notes

<details>
<summary>tokio-rs/tracing (tracing-subscriber)</summary>

###
[`v0.3.20`](https://redirect.github.com/tokio-rs/tracing/releases/tag/tracing-subscriber-0.3.20):
tracing-subscriber 0.3.20

[Compare
Source](https://redirect.github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

**Security Fix**: ANSI Escape Sequence Injection (CVE-TBD)

#### Impact

Previous versions of tracing-subscriber were vulnerable to ANSI escape
sequence injection attacks. Untrusted user input containing ANSI escape
sequences could be injected into terminal output when logged,
potentially allowing attackers to:

- Manipulate terminal title bars
- Clear screens or modify terminal display
- Potentially mislead users through terminal manipulation

In isolation, impact is minimal, however security issues have been found
in terminal emulators that enabled an attacker to use ANSI escape
sequences via logs to exploit vulnerabilities in the terminal emulator.

#### Solution

Version 0.3.20 fixes this vulnerability by escaping ANSI control
characters in when writing events to destinations that may be printed to
the terminal.

#### Affected Versions

All versions of tracing-subscriber prior to 0.3.20 are affected by this
vulnerability.

#### Recommendations

Immediate Action Required: We recommend upgrading to tracing-subscriber
0.3.20 immediately, especially if your application:

- Logs user-provided input (form data, HTTP headers, query parameters,
etc.)
- Runs in environments where terminal output is displayed to users

#### Migration

This is a patch release with no breaking API changes. Simply update your
Cargo.toml:

```toml
[dependencies]
tracing-subscriber = "0.3.20"
```

#### Acknowledgments

We would like to thank [zefr0x](http://github.com/zefr0x) who
responsibly reported the issue at `security@tokio.rs`.

If you believe you have found a security vulnerability in any tokio-rs
project, please email us at `security@tokio.rs`.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" in timezone America/New_York,
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuODIuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-08-31 08:54:22 +00:00
Remco Smits
1ca5e84019 markdown: Add HTML img tag support (#36700)
Closes #21992

<img width="1406" height="1184" alt="Screenshot 2025-08-21 at 18 09 24"
src="https://github.com/user-attachments/assets/5f14a0d8-c4d9-48ad-b10d-fadfaca258ea"
/>

Code example:

```markdown
# Html Tag
<img src="https://picsum.photos/200/300" alt="Description of image" />

# Html Tag with width and height
<img src="https://picsum.photos/200/300" alt="Description of image" width="100" height="200" />

# Html Tag with style attribute with width and height
<img src="https://picsum.photos/200/300" alt="Description of image" style="width: 100px; height: 200px" />

# Normal Tag
![alt text](https://picsum.photos/200/300)
```

Release Notes:

- Markdown: Added HTML `<img src="/some-image.svg">` tag support
2025-08-31 11:43:24 +03:00
Gerd Augsburg
d80f13242b Support for "Insert" from character key location (#37219)
Release Notes:
- Added support for the Insert-Key from a character key location for
keyboard layouts like neo2
2025-08-31 11:26:28 +03:00
Dan Dascalescu
e115584896 docs: Copyedit debugger.md and clarify settings location (#36996)
Release Notes:

- N/A
2025-08-31 11:19:25 +03:00
Jason Lee
fe0ab30e8f Fix auto size rendering of SVG images in Markdown (#36663)
Release Notes:

- Fixed auto size rendering of SVG images in Markdown.

## Before

<img width="836" height="844" alt="image"
src="https://github.com/user-attachments/assets/0782e17e-620f-4c29-a5bc-a2ffe877d220"
/>
<img width="691" height="678" alt="image"
src="https://github.com/user-attachments/assets/dbe2dd5f-fd5b-48f9-bd09-0ee35e116aec"
/>


## After

<img width="873" height="1015" alt="image"
src="https://github.com/user-attachments/assets/59cbb69f-6a81-43cb-989f-3bcea873d81e"
/>
<img width="647" height="598" alt="image"
src="https://github.com/user-attachments/assets/11b67d8e-2b6c-4245-ad13-d4616fdabf22"
/>

For GPUI example

```
cargo run -p gpui --example image
```

<img width="1212" height="740" alt="SCR-20250821-ojoy"
src="https://github.com/user-attachments/assets/62bb2847-c533-4c4d-b5f7-c9764796262a"
/>
2025-08-31 11:14:57 +03:00
Michael Sloan
253765aaa1 zeta: Improve efficiency and clarity of license detection patterns (#37242)
See discussion on #36564

Adds a simple ad-hoc substring matching pattern language which allows
skipping a bounded number of chars between matched substrings. Before
this change compiling the regex was taking ~120ms on a fast machine and
~8mb of memory. This new version is way faster and uses minimal memory.

Checked the behavior of this vs by running it against 10k licenses that
happened to be in my home dir. There were only 4 differences of behavior
with the regex implementation, and these were false negatives for the
regex implementation that are true positives with the new one.

Of the ~10k licenses in my home dir, ~1k do not match one of these
licenses, usually because it's GPL/MPL/etc.

Release Notes:

- N/A
2025-08-31 07:23:21 +00:00
Michael Sloan
ad746f25f2 zeta: Add zlib to license detection + ignore symbol differences (#37238)
See discussion on #36564. Makes the license regexes a less fragile by
not matching on symbols, while also excluding cases where a long file
ends with a valid license. Also adds Zlib license, a commented out test
to check all license-like files discovered in the homedir, and more
testcases.

Not too happy with the efficiency here, on my quite good computer it
takes ~120ms to compile the regex and allocates ~8mb for it. This is
just not a great use of regexes, I think something using eager substring
matching would be much more efficient - hoping to followup with that.

Release Notes:

- Edit Prediction: Added Zlib license to open-source licenses eligible
for data collection.
2025-08-30 14:13:39 -06:00
Cole Miller
de576bd1b8 agent: Fix agent panel header not updating when opening a history entry (#37189)
Closes #37171

Release Notes:

- agent: Fixed a bug that caused the agent information in the panel
header to be incorrect when opening a thread from history.
2025-08-30 19:51:08 +00:00
Ben Kunkle
af26b627bf settings: Improve parse errors (#37234)
Closes #ISSUE

Adds a dependency on `serde_path_to_error` to the workspace allowing us
to include the path to the setting that failed to parse on settings
parse failure.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-08-30 17:59:04 +00:00
Umesh Yadav
0a32aa8db1 language_models: Fix GitHub Copilot thread summary by removing unnecessary noop tool logic (#37152)
Closes #37025 

This PR fixes GitHub Copilot thread summary failures by removing the
unnecessary `noop` tool insertion logic. The code was originally added
as a workaround in https://github.com/zed-industries/zed/pull/30007 for
supposed GitHub Copilot API issues when tools were used previously in a
conversation but no tools are provided in the current request. However,
testing revealed that this scenario works fine without the workaround,
and the `noop` tool insertion was actually causing "Invalid schema for
function 'noop'" errors that prevented thread summarization from
working. Removing this logic eliminates the errors and allows thread
summarization to function correctly with GitHub Copilot models.

The best way to see if removing that part of code works is just
triggering thread summarisation.

Error Log:
```
2025-08-27T13:47:50-04:00 ERROR [workspace::notifications] "Failed to connect to API: 400 Bad Request {"error":{"message":"Invalid schema for function 'noop': In context=(), object schema missing properties.","code":"invalid_function_parameters"}}\n"
```

Release Notes:

- Fixed GitHub Copilot thread summary failures by removing unnecessary
noop tool insertion logic.
2025-08-30 10:42:15 -04:00
Finn Evers
b473f4a130 Fix SQL error in recent projects query (#37220)
Follow-up to https://github.com/zed-industries/zed/pull/37035

In the WSL PR, `ssh_connection_id` was renamed to
`remote_connection_id`. However, that was not accounted for within the
`recent_workspaces_query`. This caused a query fail:

```
2025-08-30T14:45:44+02:00 ERROR [recent_projects] Prepare call failed for query:
SELECT
  workspace_id,
  paths,
  paths_order,
  ssh_connection_id
FROM
  workspaces
WHERE
  paths IS NOT NULL
  OR ssh_connection_id IS NOT NULL
ORDER BY
  timestamp DESC

Caused by:
    Sqlite call failed with code 1 and message: Some("no such column: ssh_connection_id")
```

and resulted in no recent workspaces being shown within the recent
projects picker.

This change updates the column name to the new name and thus fixes the
error.

Release Notes:

- N/A
2025-08-30 13:13:23 +00:00
Joseph T. Lyons
7d0a303785 Add xAI to supported language model providers (#37206)
After setting a `grok` model via the agent panel, the settings complains
that it doesn't recognize the language model provider:

<img width="1005" height="188" alt="SCR-20250829-tqqd"
src="https://github.com/user-attachments/assets/a25fc7e0-60f0-44fd-96d2-b1cb316d06b6"
/>

Also, sorted the list, in the follow-up commit.

Release Notes:

- N/A
2025-08-30 03:03:47 +00:00
Max Brunsfeld
f78f3e7729 Add initial support for WSL (#37035)
Closes #36188

## Todo

* [x] CLI
* [x] terminals
* [x] tasks

## For future PRs
* debugging
* UI for opening WSL projects
* fixing workspace state restoration

Release Notes:

- Windows alpha: Zed now supports editing folders in WSL.

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
2025-08-29 17:18:52 -07:00
Cole Miller
1c2e2a00fe agent: Re-add workaround for language model behavior with empty tool result (#37196)
This is just copying over the same workaround here:


a790e514af/crates/agent/src/thread.rs (L1455-L1459)

Into the agent2 code.

Release Notes:

- agent: Fixed an issue where some tool calls in the Zed agent could
return an error like "`tool_use` ids were found without `tool_result`
blocks immediately after"
2025-08-29 18:26:11 -04:00
Shardul Vaidya
a70cf3f1d4 bedrock: Inference Config updates (#35808)
Fixes #36866

- Updated internal naming for Claude 4 models to be consistent.
- Corrected max output tokens for Anthropic Bedrock models to match docs

Shoutout to @tlehn for noticing the bug, and finding the resolution.

Release Notes:

- bedrock: Fixed inference config errors causing Opus 4 Thinking and
Opus 4.1 Thinking to fail (thanks [@tlehn](https://github.com/tlehn) and
[@5herlocked](https://github.com/5herlocked])
- bedrock: Fixed an issue which prevented Rules / System prompts not
functioning with Bedrock models (thanks
[@tlehn](https://github.com/tlehn) and
[@5herlocked](https://github.com/5herlocked])
2025-08-29 18:13:06 -04:00
Peter Tripp
bdedb18c30 docs: Fix msys2 (#37199)
I accidentally pushed
db508bbbe2
to main instead of to a branch.

That broke tests.

Release Notes:

- N/A
2025-08-29 21:36:22 +00:00
Peter Tripp
db508bbbe2 docs: Remove MSYS2 instructions 2025-08-29 17:29:58 -04:00
Michael Sloan
515282d719 zeta: Add detection of BSD licenses + efficiency improvements + more lenient whitespace handling (#37194)
Closes #36564

Release Notes:

- Edit Prediction: Added various BSD licenses to open-source licenses
eligible for data collection.
2025-08-29 21:16:42 +00:00
Anthony Eid
f2c3f3b168 settings ui: Start work on creating the initial structure (#36904)
## Goal 

This PR creates the initial settings ui structure with the primary goal
of making a settings UI that is
- Comprehensive: All settings are available through the UI
- Correct: Easy to understand the underlying JSON file from the UI
- Intuitive
- Easy to implement per setting so that UI is not a hindrance to future
settings changes

### Structure

The overall structure is settings layer -> data layer -> ui layer.

The settings layer is the pre-existing settings definitions, that
implement the `Settings` trait. The data layer is constructed from
settings primarily through the `SettingsUi` trait, and it's associated
derive macro. The data layer tracks the grouping of the settings, the
json path of the settings, and a data representation of how to render
the controls for the setting in the UI, that is either a marker value
for the component to use (avoiding a dependency on the `ui` crate) or a
custom render function.

Abstracting the data layer from the ui layer allows crates depending on
`settings` to implement their own UI without having to add additional UI
dependencies, thus avoiding circular dependencies. In cases where custom
UI is desired, and a creating a custom render function in the same crate
is infeasible due to circular dependencies, the current solution is to
implement a marker for the component in the `settings` crate, and then
handle the rendering of that component in `settings_ui`.

### Foundation 

This PR creates a macro and a trait both called `SettingsUi`. The
`SettingsUi` trait is added as a new trait bound on the `Settings`
trait, this allows the type system to guarantee that all settings
implement UI functionality. The macro is used to derived the trait for
most types, and can be modified through attributes for unique cases as
well.

A derive-macro is used to generate the settings UI trait impl, allowing
it the UI generation to be generated from the static information in our
code base (`default.json`, Struct/Enum names, field names, `serde`
attributes, etc). This allows the UI to be auto-generated for the most
part, and ensures consistency across the UI.


#### Immediate Follow ups

- Add a new `SettingsPath` trait that will be a trait bound on
`SettingsUi` and `Settings`
- This trait will replace the `Settings::key` value to enable
`SettingsUi` to infer the json path of it's derived type
- Figure out how to render `Option<T> where T: SettingsUi` correctly
- Handle `serde` attributes in the `SettingsUi` proc macro to correctly
get json path from a type's field and identity

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-08-29 16:56:10 -04:00
Dino
e9252a7a74 editor: Context menu aside scrolling (#35985)
Add support for scrolling the contents rendered aside an
`editor::code_context_menus::CodeContextMenu` by introducing the
`scroll_aside` method.

For now this method is only implemented for the
`CodeContextMenu::Completions` variant, which will scroll the aside
contents for an `editor::code_context_menus::CompletionsMenu` element,
as a `ScrollHandle` is added to the aside content that is rendered.

In order to be possible to trigger this via keybindings, a new editor
action is introduced, `ContextMenuScrollAside`, which accepts a number
of lines or pages to scroll the content by.

Lastly, the default keymaps for both MacOS and Linux, as well as for
Zed's vim mode, are updated to ensure that the following keybindings are
supported when a completion menu is open and the completion item's
documentation is rendered aside:

- `ctrl-e`
- `ctrl-y`
- `ctrl-d`
- `ctrl-u`

### Recording


https://github.com/user-attachments/assets/02043763-87ea-46f5-9768-00e907127b69

---

Closes #13194 

Release Notes:

- Added support for scrolling the documentation panel shown alongside
the completion menu in the editor with `cltr-d`, `ctrl-u`, `ctrl-e` and
`ctrl-y`

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: MrSubidubi <finn@zed.dev>
2025-08-29 20:23:44 +00:00
Raphael Lüthy
fcc3d1092f supermaven: Improve completion caching and position validation (#37047)
Closes #36981 

- Add completion text and position caching to reduce redundant API calls
- Only trigger new completion requests on text changes, not cursor
movement
- Validate cursor position to ensure completions show at correct
location
- Improve end-of-line range calculation for more accurate deletions
- Extract reset_completion_cache helper for cleaner code organization
- Update completion diff algorithm documentation for clarity

Edit: Sorry this is the 2nd PR, I forgot that the forks history was
messy; I cherrypicked and cleaned it properly with this PR

Release Notes:

- supermaven: Improved caching of predictions
- supermaven: Fixed an issue where changing cursor position would
incorrectly trigger new completions
2025-08-29 16:17:22 -04:00
Agus Zubiaga
a790e514af Fix ACP permission request with new tool calls (#37182)
Release Notes:

- Gemini integration: Fixed a bug with permission requests when
`always_allow_tool_calls` is enabled
2025-08-29 17:58:54 +00:00
Cole Miller
92f739dbb9 acp: Improve error reporting and log more information when failing to launch gemini (#37178)
In the case where we fail to create an ACP connection to Gemini, only
report the "unsupported version" error if the version for the found
binary is at least our minimum version. That means we'll surface the
real error in this situation.

This also fixes incorrect sorting of downloaded Gemini versions--as @kpe
pointed out we were effectively using the version string as a key. Now
we'll correctly use the parsed semver::Version instead.

Release Notes:

- N/A
2025-08-29 17:40:39 +00:00
Danilo Leal
3d4f917204 Make project symbols picker entry consistent with outline picker (#37176)
Closes https://github.com/zed-industries/zed/issues/36383

The project symbols modal didn't use the buffer font and highlighted
matches through modifying the font weight, which is inconsistent with
the outline picker, which presents code in list items in a similar way,
as well as project _and_ buffer search highlighting design.

Release Notes:

- N/A
2025-08-29 14:07:27 -03:00
Smit Barmase
a13881746a editor: APCA contrast (#37165)
Closes #35787
Closes #17890
Closes #28789
Closes #36495

How it works:

For highlights (and selections) within the visible rows of the editor,
we split them row by row. This is efficient since the number of visible
rows is constant. For each row, all highlights and selections, which may
overlap, are flattened using a line sweep. This produces non-overlapping
consecutive segments for each row, each with a blended background color.

Next, for each row, we split text runs into smaller runs to adjust its
color using APCA contrast. Since both text runs and segment are
non-overlapping and consecutive, we can use two-pointer on them to do
this.

For example, a text run for the variable red might be split into two
runs if a highlight partially covers it. As a result, one part may
appear as red, while the other appears as a lighter red, depending on
the background behind it.


Result:

<img width="1458" height="949" alt="image"
src="https://github.com/user-attachments/assets/4814c93d-12e7-4b4d-8542-d912acccfb8e"
/>

<img width="1459" height="952" alt="image"
src="https://github.com/user-attachments/assets/9e497b6c-3e66-43e8-8e5b-f634dd5ee8d3"
/>

<img width="1457" height="621" alt="image"
src="https://github.com/user-attachments/assets/8dfa6ce5-f46b-45b9-8008-66169d5aecd4"
/>

Release Notes:

- Improved text contrast when selected or highlighted in the editor.
2025-08-29 22:22:43 +05:30
Antonio Scandurra
11fb57a6d9 acp: Use the custom claude installation to perform login (#37169)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: morgankrey <morgan@zed.dev>
2025-08-29 14:16:02 +00:00
Kirill Bulatov
5001c03711 Properly process files that cannot be open for a reason (#37170)
Follow-up of https://github.com/zed-industries/zed/pull/36764

* Fix `anyhow!({e})` conversion lossing Collab error codes context when
opening a buffer remotely

* Use this context to only allow opening files that had not specific
Collab error code

Release Notes:

- N/A
2025-08-29 14:14:27 +00:00
Wouter Kayser
20d32d111c Update lsp-types to properly handle brackets (#37166)
Closes #21062

See also this pull request:
https://github.com/zed-industries/lsp-types/pull/6.

Release Notes:

- Fixed incorrect URL encoding of file paths with `[` `]` in them
2025-08-29 17:08:42 +03:00
Danilo Leal
ff035e8a22 agent: Add CC item in the settings view (#37164)
Release Notes:

- N/A
2025-08-29 09:26:52 -03:00
Kirill Bulatov
01266d10d6 Do not send any LSP logs by default to collab clients (#37163)
Follow-up https://github.com/zed-industries/zed/pull/37083

Noisy RPC LSP logs were functioning this way already, but to keep Collab
loaded even less, do not send any kind of logs to the client if the
client has a corresponding log tab not opened.

This change is pretty raw and does not fully cover scenarious with
multiple clients: if one client has a log tab open and another opens tab
with another kind of log, the 2nd kind of logs will be streamed only.
Also, it should be possible to forward the host logs to the client on
enabling — that is not done to keep the change smaller.

Release Notes:

- N/A
2025-08-29 12:23:45 +00:00
Lukas Wirth
4507f60b8d languages: Fix python activation scripts not being quoted (#37159)
Release Notes:

- N/A
2025-08-29 11:39:38 +00:00
Antonio Scandurra
d13ba0162a Require authorization for MCP tools (#37155)
Release Notes:

- Fixed a regression that caused MCP tools to run without requesting
authorization first.
2025-08-29 10:44:47 +00:00
Lukas Wirth
7403a4ba17 Add basic PyEnv and pixi support for python environments (#37156)
cc https://github.com/zed-industries/zed/issues/29807

Release Notes:

- Fixed terminals and tasks not respecting python pyenv and pixi
environments
2025-08-29 10:19:27 +00:00
Cole Miller
52da72d80a acp: Install new versions of agent binaries in the background (#37141)
Release Notes:

- acp: New releases of external agents are now installed in the
background.

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-29 04:16:49 +00:00
Mikayla Maki
384ffb883f Fix method documentation (#37140)
Release Notes:

- N/A
2025-08-29 04:07:52 +00:00
Mikayla Maki
c3ccdc0b44 Add a setting to control the number of context lines in excerpts (#37138)
Fixes https://github.com/zed-industries/zed/discussions/28739

Release Notes:

- Added a setting, `excerpt_context_lines`, for setting the number of
context lines shown in a multibuffer
2025-08-29 03:50:24 +00:00
Conrad Irwin
e5cea54cbb acp: Load agent panel even if serialized config is bogus (#37134)
Closes #ISSUE

Release Notes:

- N/A
2025-08-28 20:09:20 -06:00
Michael Sloan
9199f975dd Resolve merge conflicts 2025-08-28 18:33:43 -06:00
Michael Sloan
e045025f71 Merge branch 'main' into zeta-record-recently-active-files-when-data-collection-is-enabled 2025-08-28 18:30:46 -06:00
Michael Sloan
cfd56a744d zeta: Show update required notification on appropriate window(s) (#37130)
To show these notifications, Zeta was being initialized with the initial
workspace it's used on - which may not even still exist! This removes a
confusing/misleading workspace field from Zeta.

Release Notes:

- N/A
2025-08-29 00:22:56 +00:00
Michael Sloan
177b0d7f5e Estimate time spent navigating and editing code 2025-08-28 17:43:43 -06:00
Marshall Bowers
960d9ce48c Disable Expert language server by default for Elixir (#37126)
This PR updates the language server configuration for Elixir and HEEx to
not start the [Expert](https://github.com/elixir-lang/expert) language
server by default.

While Expert is the official Elixir language server, it is still early,
so we don't want to make it the default just yet.

Release Notes:

- Updated the default Elixir and HEEx language server settings to not
start the Expert language server.
2025-08-28 22:50:27 +00:00
Marshall Bowers
52d119b637 docs: Add Expert to Elixir docs (#37127)
This PR adds documentation for
[Expert](https://github.com/elixir-lang/expert) to the Elixir docs.

Also updated the examples for the other language servers to be
representative of all the supported language servers.

Release Notes:

- N/A
2025-08-28 22:45:09 +00:00
Richard Feldman
8c18f059f1 Always enable acp accept/reject buttons for now (#37121)
We have a bug in our ACP implementation where sometimes the
Accept/Reject buttons are disabled (and stay disabled even after the
thread has finished). I haven't found a complete fix for this yet, so in
the meantime I'm putting out the fire by making it so those buttons are
always enabled. That way you're never blocked, and the only consequence
of the bug is that sometimes they should be disabled but are enabled
instead.

Release Notes:

- N/A
2025-08-28 21:42:12 +00:00
Cole Miller
930189ed83 acp: Support automatic installation of Claude Code (#37120)
Release Notes:

- N/A
2025-08-28 21:38:14 +00:00
Ben Brandt
08c23c92ca acp: Bump to 0.1.1 (#37119)
No big changes, just tracking the latest version after the official
release

Release Notes:

- N/A
2025-08-28 21:16:06 +00:00
Julia Ryan
88e8f7af68 Activate preview for initially selected item (#37112)
@JosephTLyons pointed out that it's a bit weird that we only show a
preview for items selected after the initial one, so this does it for
that too.

It makes tab switching feel even faster!

Release Notes:

- N/A

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-28 21:07:02 +00:00
Finn Evers
f2e62c98d1 docs: Fix broken link in agent-panel.md (#37113)
This fixes a small typo I stumbled upon, which caused a 404 within the
docs.

Release Notes:

- N/A
2025-08-28 19:48:35 +00:00
Cole Miller
8697b91ea0 acp: Automatically install gemini under Zed's data dir (#37054)
Closes: https://github.com/zed-industries/zed/issues/37089

Instead of looking for the gemini command on `$PATH`, by default we'll
install our own copy on demand under our data dir, as we already do for
language servers and debug adapters. This also means we can handle
keeping the binary up to date instead of prompting the user to upgrade.

Notes:

- The download is only triggered if you open a new Gemini thread
- Custom commands from `agent_servers.gemini` in settings are respected
as before
- A new `agent_servers.gemini.ignore_system_version` setting is added,
similar to the existing settings for language servers. It's `true` by
default, and setting it to `false` disables the automatic download and
makes Zed search `$PATH` as before.
- If `agent_servers.gemini.ignore_system_version` is `false` and no
binary is found on `$PATH`, we'll fall back to automatic installation.
If it's `false` and a binary is found, but the version is older than
v0.2.1, we'll show an error.

Release Notes:

- acp: By default, Zed will now download and use a private copy of the
Gemini CLI binary, instead of searching your `$PATH`. To make Zed search
your `$PATH` for Gemini CLI before attempting to download it, use the
following setting:

```
{
  "agent_servers": {
    "gemini": {
      "ignore_system_version": false
    }
  }
}
```
2025-08-28 19:33:00 +00:00
Michael Sloan
47aaaa8bcf Make SanitizedPath wrap Path instead of Arc<Path> to avoid allocation (#37106)
Release Notes:

- N/A
2025-08-28 13:32:30 -06:00
Conrad Irwin
69933d5b81 Add support for Claude Code auth (#37103)
Co-authored-by: Antonio Scandurra <me@as-cii.com>

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-28 12:59:31 -06:00
Joseph T. Lyons
909d7215c0 Update patch and nightly release docs (#37109)
Release Notes:

- N/A
2025-08-28 18:49:03 +00:00
Richard Feldman
27777d4b8f Have ACP respect always_allow_tool_actions (#37104)
Release Notes:

- ACP agents now respect the always_allow_tool_actions setting
2025-08-28 14:18:25 -04:00
Marshall Bowers
4469b14512 collab_ui: Show channel list while reconnecting (#37107)
This PR makes it so the channel list will still be shown while
reconnecting to Collab instead of showing the signed-out state.

In order to model the transitional states that occur while reconnecting,
we needed to introduce a new `Status::Reauthenticated` state that we go
through when signing in as part of a reconnect. This is because we
cannot tell from `Status::Authenticated` alone if we're authenticating
for the first time or reauthenticating.

Release Notes:

- N/A
2025-08-28 18:15:08 +00:00
Finn Evers
29fc324a78 html: Bump to v0.2.2 (#37102)
This PR bumps the HTML extension to v0.2.2.

Changes:

- https://github.com/zed-industries/zed/pull/28184
- https://github.com/zed-industries/zed/pull/36948
- https://github.com/zed-industries/zed/pull/37098

Release Notes:

- N/A
2025-08-28 17:07:06 +00:00
Finn Evers
4ef9294123 html: Add outline (#37098)
We were missing an outline definition for HTML flies, hence this PR adds
one for that

<img width="255" height="726" alt="image"
src="https://github.com/user-attachments/assets/ae59cb8d-6c69-4019-966a-d5baf744329d"
/>

Release Notes:

- N/A
2025-08-28 18:44:30 +02:00
Kai Ren
4b0609840b go: Fix highlighting of fields (#37026)
Closes #36420

## Synopsis

The issue in #36420 is caused by #7276, which bound the appropriate
tree-sitter queries to the `@variable.member` color. However, I have
found neither this color's declaration nor its other usages in the
codebase (neither on the latest `main` nor on
79c1003b34).

Other languages use for such situations the `@property` color.


## Solution

Just change the used `@variable.member` color to the `@property` one.

Seems fully inline with the changes illustrated in #7276.


## Screenshots

<img width="856" height="465" alt="Screenshot 2025-08-28 at 13 18 38"
src="https://github.com/user-attachments/assets/9d1f3542-8749-421f-864f-959c1242cc64"
/>

<img width="837" height="462" alt="Screenshot 2025-08-28 at 13 20 08"
src="https://github.com/user-attachments/assets/36a80c22-9de9-46b1-87e3-7fdeaa62978f"
/>


## Changelog

Release Notes:

- go: Fixed highlighting of fields.
2025-08-28 17:40:48 +02:00
Liam
2cb697e9f4 copilot: Use updated Copilot Chat model schema (#33007)
Use the latest Copilot Chat model schema, matching what is used in
VSCode, to get more data about available models than was previously
accessible. Replace hardcoded default model (gpt-4.1) with the default
model included in JSON. Other data like premium request multipliers
could be used in the future if Zed implements a way for models to
display additional details about themselves, such as with tooltips on
hover.

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-08-28 15:32:15 +00:00
Umesh Yadav
c8e99125bd language_models: Fix tool calling for x-ai/grok-code-fast-1 model via OpenRouter (#37094)
Closes #37022
Closes #36994

This update ensures all Grok models use the JsonSchemaSubset format for
tool schemas.

A previous fix for this issue was too specific, only targeting grok-4
models. This caused other variants, like grok-code-fast-1, to be missed.
We've now broadened the logic to correctly apply the setting to the
entire Grok model family.

Release Notes:

- Fix tool calling for `x-ai/grok-code-fast-1` model via OpenRouter.
2025-08-28 11:28:22 -04:00
Lukas Wirth
835e5ba662 Inject venv environment via the toolchain (#36576)
Instead of manually constructing the venv we now ask the python
toolchain for the relevant information, unifying the approach of vent
inspection

Fixes https://github.com/zed-industries/zed/issues/27350

Release Notes:

- Improved the detection of python virtual environments for terminals
and tasks in remote projects.
2025-08-28 14:40:43 +00:00
Umesh Yadav
24ee98b3e1 agent2: Fix model deduplication to use provider ID and model ID (#37088)
Closes #37043

Previously claude sonnet 4 was missing from copilot as it was colliding
with zed's claude-sonnet-4 model id. Now we do deduplication based upon
model and provider id both.

| Before | After |
|--------|--------|
| <img width="784" height="950" alt="CleanShot 2025-08-28 at 18 31
28@2x"
src="https://github.com/user-attachments/assets/d49d5a17-7271-417d-bb5e-bc380071e810"
/> | <img width="720" height="876" alt="CleanShot 2025-08-28 at 18 31
42@2x"
src="https://github.com/user-attachments/assets/a5100c05-994e-4e19-ab20-34c0258b977c"
/> |

Release Notes:

- Fixed an issue where models with the same ID from different providers
(such as Claude Sonnet 4 from both Zed and Copilot) were incorrectly
deduplicated in the model selector—now all variants are shown.
2025-08-28 16:12:59 +02:00
Danilo Leal
213ee32b94 docs: Make unsupported features more prominent in external agents (#37090)
Use the notes component to better highlight that in the docs UI.

Release Notes:

- N/A
2025-08-28 10:47:19 -03:00
Kirill Bulatov
f127ba82d1 Remote LSP logs (#37083)
Take 2: https://github.com/zed-industries/zed/pull/36709 but without the
very bad `cfg`-based approach for storing the RPC logs.

--------------

Enables LSP log tracing in both remote collab and remote ssh
environments.
Server logs and server RPC traces can now be viewed remotely, and the
LSP button is now shown in such projects too.

Closes https://github.com/zed-industries/zed/issues/28557

Co-Authored-By: Kirill <kirill@zed.dev>
Co-Authored-By: Lukas <lukas@zed.dev>

Release Notes:

- Enabled LSP log tracing in both remote collab and remote ssh
environments

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-28 15:32:44 +03:00
Antonio Scandurra
39d86eeb7f Trim API key when submitting requests to LLM providers (#37082)
This prevents the common footgun of copy/pasting an API key
starting/ending with extra newlines, which would lead to a "bad request"
error.

Closes #37038 

Release Notes:

- agent: Support pasting language model API keys that contain newlines.
2025-08-28 12:00:44 +00:00
Antonio Scandurra
4981c33bf3 acp: Don't cancel editing when scrolling message out of view (#37020)
Release Notes:

- agent: Fixed a bug that canceled editing when scrolling the user
message out of view.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-28 12:57:09 +02:00
Lorenzo Stella
54609d4d00 Fix boolean settings in "Agent Settings" documentation page (#37068)
This fixes some errors in the examples in the "Agent Settings" page at
https://zed.dev/docs/ai/agent-settings#agent-settings, where strings
"true" and "false" are used in place of the proper boolean JSON values:
strings don't work for all those settings, and are marked as errors when
editing settings.json, while booleans do work.

Release Notes:

- N/A
2025-08-28 10:35:30 +00:00
张小白
ff03dda90a Refactor KeybindingKeystroke (#37065)
This pull request refactors the `KeybindingKeystroke` struct and related
code to improve platform abstraction. The changes centralize
platform-specific logic within `KeybindingKeystroke` and update its
usage throughout the codebase, making the API more consistent and less
error-prone.



Release Notes:

- N/A
2025-08-28 08:40:43 +00:00
Anthony Eid
73b38c8306 debugger: Add ability to only show stack frame entries from visible work trees (#37061)
This PR adds a toggleable filter to the stack frame list that filters
out entries that don't exist within a user's project (visible work
trees). This works by keeping a vector of entry indices that exist
within a user's project and updates the list state based on these
entries when filtering the list.

I went with this approach so the stack frame list wouldn't have to
rebuild itself whenever the filter is toggled and it could persist its
state across toggles (uncollapsing a collapse list). It was also easier
to keep track of selected entries on toggle using the vector as well.

### Preview

https://github.com/user-attachments/assets/d86c7485-c885-4bbb-bebb-2f6385674925



Release Notes:

- debugger: Add option to only show stack frames from user's project in
stack frame list
2025-08-28 07:53:32 +00:00
张小白
38e5c8fb66 keymap_editor: Fix incorrect keystroke being reported (#36998)
This PR fixes two bugs and also changes one behavior in the **Keymap
Editor**.

As shown in the video, when I press `ctrl-shift-2` in the Keymap Editor,
the first keystroke is displayed as `ctrl-shift-@`, which is incorrect.
On macOS and Linux, it should be `ctrl-@`.



https://github.com/user-attachments/assets/69cfcfa0-b422-45d6-8e69-80f8608180fd



Also, after pressing `ctrl-shift-2` and then releasing `2` and `ctrl`, a
`shift` keystroke was incorrectly added.



https://github.com/user-attachments/assets/892124fd-847d-4fde-9b20-a27ba49ac934



Now, when you enter a sequence like `+ctrl+alt-alt+f` in the Keymap
Editor, it will output `ctrl-f` instead of `ctrl-alt-f`, matching VS
Code’s behavior.


Release Notes:

- Fixed incorrect keystroke reporting in the Keymap Editor.
2025-08-28 15:16:13 +08:00
Gaauwe Rombouts
78c2f1621d Add macOS window tabs (#33334)
Closes https://github.com/zed-industries/zed/issues/14722
Closes https://github.com/zed-industries/zed/issues/4948
Closes https://github.com/zed-industries/zed/issues/7136

Follow up of https://github.com/zed-industries/zed/pull/20557 and
https://github.com/zed-industries/zed/pull/32238.

Based on the discussions in the previous PRs and the pairing session
with @ConradIrwin I've decided to rewrite it from scratch, to properly
incorporate all the requirements. The feature is opt-in, the settings is
set to false by default. Once enabled via the Zed settings, it will
behave according to the user’s system preference, without requiring a
restart — the next window opened will adopt the new behavior (similar to
Ghostty).

I’m not entirely sure if the changes to the Window class are the best
approach. I’ve tried to keep things flexible enough that other
applications built with GPUI won’t be affected (while giving them the
option to still use it), but I’d appreciate input on whether this
direction makes sense long-term.



https://github.com/user-attachments/assets/9573e094-4394-41ad-930c-5375a8204cbf

### Features
* System-aware tabbing behavior
* Respects the three system modes: Always, Never, and Fullscreen
(default on macOS)
* Changing the Zed setting does not require a restart — the next window
reflects the change
* Full theme support
    * Integrates with light and dark themes
* [One
Dark](https://github.com/user-attachments/assets/d1f55ff7-2339-4b09-9faf-d3d610ba7ca2)
* [One
Light](https://github.com/user-attachments/assets/7776e30c-2686-493e-9598-cdcd7e476ecf)
    * Supports opaque/blurred/transparent themes as best as possible
* [One Dark -
blurred](https://github.com/user-attachments/assets/c4521311-66cb-4cee-9e37-15146f6869aa)
* Dynamic layout adjustments
    * Only reserves tab bar space when tabs are actually visible
* [With
tabs](https://github.com/user-attachments/assets/3b6db943-58c5-4f55-bdf4-33d23ca7d820)
* [Without
tabs](https://github.com/user-attachments/assets/2d175959-5efc-4e4f-a15c-0108925c582e)
* VS Code compatibility
* Supports the `window.nativeTabs` setting in the VS Code settings
importer
* Command palette integration
    * Adds commands for managing tabs to the command palette
* These can be assigned to keyboard shortcuts as well, but didn't add
defaults as to not reserve precious default key combinations

Happy to pair again if things can be improved codewise, or if
explanations are necessary for certain choices!



Release Notes:
* Added support for native macOS window tabbing. When you set
`"use_system_window_tabs": true`, Zed will merge windows in the same was
as macOS: by default this happens only when full screened, but you can
adjust your macOS settings to have this happen on all windows.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-28 03:51:22 +00:00
Dino
0a9f407872 search: Add support for case-sensitivity pattern items (#34762)
This Pull Request introduces support for pattern items in the buffer
search. It does so by splitting the `query` methods into two new
methods:

- `BufferSearchBar.raw_query` – returns the text from the search query
editor
- `BufferSearchBar.query` - returns the search query with pattern items
removed

Whenever the search query is updated, processing of the
`EditorEvent::Edited` event ends up calling the
`BufferSearchBar.apply_pattern_items` method, which parses the pattern
items from the raw query, and updates the buffer search bar's search
options accordingly. This `apply_pattern_items` function avoids updating
the `BufferSearchBar.default_options` field in order to be able to reset
the search options when a pattern items is removed. Lastly, new pattern
items can easily be added by updating the `PATTERN_ITEMS` array.

### Screen Capture


https://github.com/user-attachments/assets/ebd83c38-e480-4c24-9b8c-6edde69cf392

---

Closes #32390

Release Notes:

- Added support for the `\c` and `\C` query pattern items to control
case-sensitivity in buffer search

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-27 19:27:02 -06:00
Romans Malinovskis
4e1a901059 helix: Improve "x" behavior (#35611)
Closes #32020 

Release Notes:
- Helix: Improve `x` behaviour. Will respect modifiers (`5 x`). Pressing
`x` on a empty line, will select current+next line, because helix
considers current line to be already selected without the need of
pressing `x`.
2025-08-27 20:25:00 -05:00
Michael Sloan
8af212e785 Fix watching of Git repo in presence of scanner restarts (#37052)
The scanner is restarted after loading initial settings, and there was
an optimization to not re-discover and re-watch git repositories if they
already exist in the snapshot. #35865 added cleanup of watches that
occurred when the scanner restarts, and so in some cases repos were no
longer watched.

Release Notes:

- Linux: Fixed a case where Git repositories might not be watched for
changes, causing branch switching to not update the UI.

Co-authored-by: Julia <julia@zed.dev>
2025-08-27 18:34:14 -06:00
Kirill Bulatov
b233df8343 Revert "Remote LSP logs (#36709)" (#37051)
This reverts commit e2bf8e5d9c.

See
https://github.com/zed-industries/zed/pull/37050#issuecomment-3230017137
for the context: musl builds started to fail and the amount of `cfg!`s
to fix this is too large.

Instead, the lsp_log.rs has to be split and repurposed better for the
remote headless server.

Release Notes:

- N/A
2025-08-27 23:24:19 +00:00
Finn Evers
9a97f9465b rust: Improve highlighting within macros (#37049)
This makes sure we do not apply the highlights for snake case
identifiers as well as paths for attributes too broadly to all types of
macros, which should make macros much more readable overall whilst
keeping the highlighting for the attribute items.

| Before | After |
| --- | --- |
| <img width="1414" height="958" alt="Bildschirmfoto 2025-08-28 um 00 37
58"
src="https://github.com/user-attachments/assets/1254b9a2-d07a-4be4-9b4f-555a7c640302"
/> | <img width="1414" height="958" alt="Bildschirmfoto 2025-08-28 um 00
37 38"
src="https://github.com/user-attachments/assets/5f6dd66c-5469-4f27-9f1d-0a6e6e8d8085"
/> |

Release Notes:

- rust: Improved highlighting within macros.
2025-08-28 00:57:08 +02:00
tidely
48299b5b24 search: Preserve SearchOptions across dismisses (#36954)
Closes #36931 and #21956

Preserves `SearchOptions` across dismisses of the buffer search bar.
This behavior is consistent with VSCode, which seems reasonable. The
`configured_options` field is then no longer being used. The
configuration is still read during initialization of the
`BufferSearchBar`, but not after.

Something to consider is that there are other elements in the search bar
which are not kept across dismisses such as replace status. However
these are visually separated in the UI, leading me to believe this is a
okay change to make.

Release Notes:

- Preserve search options between buffer search dismisses
2025-08-28 00:17:21 +03:00
Jordan Pittman
4e4bfd6f4e editor: Add "Wrap Selections in Tag" action (#36948)
This PR adds the ability for a user to select one or more blocks of text
and wrap each selection in an HTML tag — which works by placing multiple
cursors inside the open and close tags so the appropriate element name
can be typed in to all places simultaneously.

This is similar to the emmet "Wrap with Abbreviation" functionality
discussed in #15588 but is a simpler version that does not rely on
Emmet's language server.

Here's a preview of the feature in action:


https://github.com/user-attachments/assets/1931e717-136c-4766-a585-e4ba939d9adf


Some notes and questions:
- The current implementation is a hardcoded with regards to supported
languages. I'd love some direction on how much of this information to
push into the relevant language structs.
- I can see this feature as something that languages added by an
extension would want to enable support for — is this something you'd
want?
- The syntax is hardcoded to support HTML/XML/JSX-like languages. I
don't suppose this is a problem but figured I'd point it out anyway.
- I called it "Wrap in tag" but open to whatever naming you feel is
appropriate.
- The implementation doesn't use `manipulate_lines` — I wasn't sure how
make use of that without extra overhead / bookkeeping — does this seem
fine?
- I could also investigate adding wrap in abbreviation support by
communicating with the Emmet language server but I think I'll need some
direction on how to handle Emmet's custom LSP message. I could do this
either in addition to or instead of this feature — though imo this
feature is a nice "shortcut" regardless.

Release Notes:

- Added a new "Wrap Selections in Tag" action that lets you wrap one or
more selections in tags based on language. Works in HTML, JSX, and
similar languages, and places cursors inside both opening and closing
tags so you can type the tag name once and apply it everywhere.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-08-28 02:37:32 +05:30
Piotr Osiewicz
5444fbd8fe python: Look for local venvs in all directories between root of the worktree and current pyproject.toml (#37037)
cc @michael-ud - if you can build Zed, I'd appreciate it if you could
give this a go with your project. Otherwise I can provide a link to
download of current nightly via an e-mail for you to try out (if you
want).
This change will land in Preview (if merged) on next Wednesday and then
it'll be in Stable a week after that.
Related to: #20402
Release Notes:

- python: Zed now searches for virtual environments in intermediate
directories between a root of the worktree and the location of
pyproject.toml applicable to the currently focused file.
2025-08-27 22:59:56 +02:00
Joseph T. Lyons
58f896e5cd Update Wednesday release process docs (#37033)
Release Notes:

- N/A
2025-08-27 15:59:08 -04:00
Michael Sloan
05b066aa98 Only track recent editors if data collection is enabled 2025-08-27 13:50:15 -06:00
Joseph T. Lyons
d43cf2c486 Link out to release channel FAQ in Docs (#37029)
This PR links users to the FAQ on the release channels, which has more
in-depth coverage of the process.

Release Notes:

- N/A
2025-08-27 19:10:10 +00:00
Ben Kunkle
e2bf8e5d9c Remote LSP logs (#36709)
Enables LSP log tracing in both remote collab and remote ssh
environments.
Server logs and server RPC traces can now be viewed remotely, and the
LSP button is now shown in such projects too.

Closes https://github.com/zed-industries/zed/issues/28557

Co-Authored-By: Kirill <kirill@zed.dev>
Co-Authored-By: Lukas <lukas@zed.dev>

Release Notes:

- Enabled LSP log tracing in both remote collab and remote ssh
environments

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-08-27 21:55:34 +03:00
Cole Miller
c158eb2442 docs: Note that Gemini CLI is not supported over SSH (#37023)
Release Notes:

- N/A
2025-08-27 14:34:40 -04:00
Peter Tripp
71f900346c Add ';' and '*' to word_chars to improve softwrap (#37024)
Follow-up to: https://github.com/zed-industries/zed/pull/37019
See also: https://github.com/zed-industries/zed/issues/37010

Before/After:
<img width="418" height="402" alt="Screenshot 2025-08-27 at 13 54 52"
src="https://github.com/user-attachments/assets/1b2e02dd-c216-4372-b23e-5a3a619d2b77"
/>


Release Notes:

- N/A
2025-08-27 18:12:34 +00:00
Michael Sloan
56ac4320fa Misc cleanup 2025-08-27 11:41:51 -06:00
Floyd Wang
9ca4fb16b2 gpui: Support disabling window resizing and minimizing (#36859)
Add support to disable both window resizing and minimizing.

| | macOS | Windows |
| - | - | - |
| **Unresizable** | <img width="412" height="440"
alt="SCR-20250822-qpea"
src="https://github.com/user-attachments/assets/d6d45510-dc4b-436f-a9fa-ce9cb0b0c411"
/> | <img width="276" height="298" alt="2025-08-22 110757"
src="https://github.com/user-attachments/assets/9deff498-e903-4173-9c26-072dd9409fc1"
/> |
| **Unminimizable** | <img width="412" height="440"
alt="SCR-20250822-qpfl"
src="https://github.com/user-attachments/assets/e1d5f9eb-6de5-4908-8b52-38ccb2e65689"
/> | <img width="276" height="298" alt="2025-08-22 110814"
src="https://github.com/user-attachments/assets/da94b006-3544-4274-8b02-1cab7ca8dd70"
/> |

Release Notes:

- N/A
2025-08-27 10:26:57 -07:00
Julia Ryan
45ff22f793 Add bang to word chars for wrapping (#37019)
Fixes #37010

Release Notes:

- N/A
2025-08-27 17:17:34 +00:00
Umesh Yadav
fead511df9 docs: Update Gemini CLI version requirements and install instructions (#37008)
Gemini cli - 0.2.0 is no longer in preview it's the latest version and
released as of today.

Release Notes:

- N/A

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-08-27 12:59:30 -04:00
Cole Miller
07373d15ef acp: Fix gemini process being leaked (#37012)
Release Notes:

- acp: Fixed a bug that caused external agent server subprocesses to be
leaked.

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-27 16:21:28 +00:00
Bennet Bo Fenner
b5e9b65e8c acp: Fix model selector sometimes showing no models (#37006)
Release Notes:

- acp: Fix an issue where the model selector would sometimes be empty

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-27 15:39:39 +00:00
Smit Barmase
5d7f12ce88 project: Add dynamic capabilities registration for "workspace/didChangeWorkspaceFolders" (#37005)
Fixes missing capability registration for
"workspace/didChangeWorkspaceFolders".

```
WARN  [project::lsp_store] unhandled capability registration: Registration { id: "e288546c-4458-401a-a029-bbba759d5a71", method: "workspace/didChangeWorkspaceFolders", register_options: Some(Object {}) }
```

We already correctly send back events to server on workspace add and
remove by checking this capability.
cf89691b85/crates/lsp/src/lsp.rs (L1353)


cf89691b85/crates/lsp/src/lsp.rs (L1388)

Release Notes:

- N/A
2025-08-27 21:01:36 +05:30
Peter Tripp
1b9c471204 Fix 'Edit in Debug.json' in debugger::Start modal (#37002)
Closes https://github.com/zed-industries/zed/issues/36992

Release Notes:

- N/A
2025-08-27 10:51:26 -04:00
Bennet Bo Fenner
8cf663011f acp: Add more logs to model selector to diagnose issue (#36997)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
Co-authored-by: Katie Geer <katie@zed.dev>
2025-08-27 12:53:07 +00:00
Finn Evers
54f9b67de2 docs: Document more settings (#36993)
Within our hosted docs, we are missing documentation for quite a lot of
settings - sometimes for newer settings, sometimes for settings that are
more than two years old. This leads (amongst other things) to feature
requests for features that are already supported, false issue reports
(because people couldn't find the setting for what caused the issue
within the documentation) and generally just takes time for for both
these affected by the missing documentation as well as these handling
the questions around it.

This change here takes a stab at the problem by adding more
documentation for a lot supported setting (not all of it) as well as
reorganizing some settings so that some stuff can (hopefully) be found
more easily. Eventually, we should find a better method for this, but
it's still better than informing people for the n-th time that we e.g.
have `agent_font_size` for the agent panel. Manually audited twice but
I'll take another thorough look before merging.

Release Notes:

- N/A
2025-08-27 14:51:06 +02:00
Danilo Leal
d99a17e357 docs: Add ACP-related content (#36966)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Matt Miller <mattrx@gmail.com>
2025-08-27 14:12:52 +02:00
Bennet Bo Fenner
c72e594afe acp: Fix model selector sometimes showing no models (#36995)
Release Notes:

- N/A
2025-08-27 13:08:03 +02:00
Antonio Scandurra
b4d4294bee Restore token count for text threads (#36989)
Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-27 09:29:17 +00:00
Antonio Scandurra
e5c0614e88 Ensure we use the new agent when opening the panel for the first time (#36988)
Release Notes:

- N/A
2025-08-27 09:18:15 +00:00
Smit Barmase
ea347b0aa1 project: Handle capabilities parse for more methods when registerOptions doesn't exist (#36984)
Closes #36938

Follow up to https://github.com/zed-industries/zed/pull/36554

When `registerOptions` is `None`, we should fall back instead of
skipping capability registration.

1. `Option<OneOf<bool, T>>`, where `T` is struct – handled in the
attached PR 
2. `Option<T>`, where `T` is an enum that can be `Simple(bool)` or
`Options(S)` – this PR 
3. `Option<T>`, where `T` is struct – we should fall back to default
values for these options ⚠️

Release Notes:

- Fixed an issue where hover popovers would not appear in language
servers like Java.
2025-08-27 13:00:10 +05:30
Finn Evers
a03897012e Swap NewlineBelow and NewlineAbove bindings for default linux keymap (#36939)
Closes https://github.com/zed-industries/zed/issues/33725

The default bindings for the `editor::NewlineAbove` and
`editor::NewlineBelow` actions in the default keymap were accidentally
swapped some time ago. This causes confusion, as normally these are the
other way around.

This PR fixes this by swapping these back, which also matches what
[VSCode does by
default](https://code.visualstudio.com/shortcuts/keyboard-shortcuts-linux.pdf).

Release Notes:

- Swapped the default bindings for `editor::NewlineBelow` and
`editor::NewlineAbove` for Linux and Windows to align more with other
editors.
2025-08-27 07:06:33 +00:00
Conrad Irwin
f4071bdd8e acp: Upgrade errors (#36980)
- **Pass --engine-strict to gemini install command**
- **Make it clearer that if upgrading fails, you need to fix i**

Closes #ISSUE

Release Notes:

- N/A
2025-08-27 00:24:56 -06:00
Michael Sloan
8b2147aeef Send additional context for edit predictions data via a telemetry event
Also:

* Removes old PredictEditsBody fields that don't have anticipated future use

* Sorts diagnostics by proximity to cursor and truncates based on json byte count

* Brings back cursor_offset
2025-08-27 00:09:08 -06:00
Caio Piccirillo
abd6009b41 Enhance syntax highlight for C++20 keywords (#36817)
Closes #36439 and #32999 

## C++20 modules:
Before (Zed Preview v0.201.3):
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/8eaaf77f-4e27-4a5a-9e87-4e5ba7293990"
/>
After:
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/df8d0b2c-f2d0-4b0e-9a52-495e6be5a8c0"
/>

## C++20 coroutines:
Before (Zed Preview v0.201.3):
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/652191ec-a653-444d-a239-da3e4e4b661e"
/>
After:
<img width="1048" height="704" alt="image"
src="https://github.com/user-attachments/assets/36947eb5-8997-483a-b36c-8af84872b158"
/>

## Logical operators:
Before (Zed Preview v0.201.3):
<img width="511" height="102" alt="image"
src="https://github.com/user-attachments/assets/9bf95bac-b076-4edd-a1f3-c3dfee98c2fd"
/>

After:
<img width="511" height="102" alt="image"
src="https://github.com/user-attachments/assets/82c7564d-b94d-41f5-9c48-e39fe3ba3b3e"
/>

## Operator keyword:
Before (Zed Preview v0.201.3):
<img width="591" height="381" alt="image"
src="https://github.com/user-attachments/assets/1d9dad05-2d86-4566-97f4-aff440dcd1df"
/>

After:
<img width="591" height="381" alt="image"
src="https://github.com/user-attachments/assets/a1ca289a-8a5d-4ffd-96db-0d511405da4b"
/>

## Goto:
Before (Zed Preview v0.201.3):
<img width="610" height="430" alt="image"
src="https://github.com/user-attachments/assets/2d00382b-d1ad-4e36-a3ee-88e06ec528ed"
/>

After:
<img width="610" height="430" alt="image"
src="https://github.com/user-attachments/assets/de887b21-66f0-4a70-9ed2-e18dbb3c81c9"
/>

Release Notes:

- Enhance keyword highlighting for C++
2025-08-27 04:31:57 +00:00
Joseph T. Lyons
a3e1611fa8 Bump Zed to v0.203 (#36975)
Release Notes:

- N/A
2025-08-27 02:52:24 +00:00
Conrad Irwin
e6e64017ea acp: Require gemini version 0.2.0 (#36960)
Release Notes:

- N/A
2025-08-27 02:01:51 +00:00
Danilo Leal
d0aef3cec1 thread view: Fix cut-off review button (#36970) 2025-08-26 22:17:03 -03:00
Max Brunsfeld
1eae76e856 Restructure remote client crate, consolidate SSH logic (#36967)
This is a pure refactor that consolidates all SSH remoting logic such
that it should be straightforward to add another transport to the
remoting system.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-08-27 00:15:39 +00:00
Joseph T. Lyons
d713390366 Add get stable channel release notes script (#36969)
Release Notes:

- N/A
2025-08-26 23:35:29 +00:00
Danilo Leal
9614b72b06 thread view: Add one more UI clean up pass (#36965)
Release Notes:

- N/A
2025-08-26 19:43:07 -03:00
Michael Sloan
7447dc81d9 Clippy 2025-08-26 15:49:21 -06:00
Michael Sloan
952a26998d Include last cursor position in recent file entries + handle multibuffers 2025-08-26 15:46:14 -06:00
Daniel Dye
d7c735959e Add xAI's Grok Code Fast 1 model (#36959)
Release Notes:

- Add the `grok-code-fast-1` model to xAI's list of available models.
2025-08-26 21:08:45 +00:00
Danilo Leal
d8847192c8 thread view: Adjust thinking block UI (#36958)
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-26 17:35:56 -03:00
Michael Sloan
dd607049f8 Make entry_for_path return a reference instead of cloning 2025-08-26 14:31:39 -06:00
Danilo Leal
bd4e943597 acp: Add onboarding modal & title bar banner (#36784)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-26 16:59:12 -03:00
Danilo Leal
c5d3c7d790 thread view: Improve agent installation UI (#36957)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-26 16:58:23 -03:00
张小白
fff0ecead1 windows: Fix keystroke & keymap (#36572)
Closes #36300

This PR follows Windows conventions by introducing
`KeybindingKeystroke`, so shortcuts now show up as `ctrl-shift-4`
instead of `ctrl-$`.

It also fixes issues with keyboard layouts: when `use_key_equivalents`
is set to true, keys are remapped based on their virtual key codes. For
example, `ctrl-\` on a standard English layout will be mapped to
`ctrl-ё` on a Russian layout.


Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
2025-08-27 03:24:50 +08:00
Michael Sloan
09b3650f6c Remove cursor_offset 2025-08-26 13:05:53 -06:00
Michael Sloan
884e7e6750 Filter out rapid changes in active item 2025-08-26 12:00:39 -06:00
Max Brunsfeld
b1b60bb7fe Work around duplicate ssh projects in workspace migration (#36946)
Fixes another case where the sqlite migration could fail, reported by
@SomeoneToIgnore.

Release Notes:

- N/A
2025-08-26 10:54:39 -07:00
Michael Sloan
5b97ebb109 Include cursor position of current file 2025-08-26 11:50:46 -06:00
Adam Mulvany
0e575b2809 helix: Fix buffer search: deploy reset to normal mode (#36917)
## Fix: Preserve Helix mode when using  search

### Problem
When using `buffer search: deploy` in Helix mode, pressing Enter to
dismiss the search incorrectly returned to Vim NORMAL mode instead of
Helix NORMAL mode.

### Root Cause
The `search_deploy` function was resetting the entire `SearchState` to
default values when buffer search: deploy was activated. Since the
default `Mode` is `Normal`, this caused `prior_mode` to be set to Vim's
Normal mode regardless of the actual mode before search.

### Solution
Modified `search_deploy` to preserve the current mode when resetting
search state:
- Store the current mode before resetting
- Reset search state to default
- Restore the saved mode to `prior_mode`

This ensures the editor returns to the correct mode (Helix NORMAL or Vim
NORMAL) after dismissing buffer search.

### Settings

I was able to reproduce and then test the fix was successful with the
following config and have also tested with vim: default_mode commented
out to ensure that's not influencing the mode selection flow:

```
  "helix_mode": true,
  "vim_mode": true,
  "vim": {
    "default_mode": "helix_normal"
  },
```

This is on Kubuntu 24.04.

The following test combinations pass locally:

- `cargo test -p search`
- `cargo test -p vim` 
- `cargo test -p editor`
- `cargo test -p workspace`
- `cargo test -p gpui -- vim`
- `cargo test -p gpui -- helix`

Release Notes:

- Fixed Helix mode switching to Vim normal mode after using `buffer
search: deploy` to search

Closes #36872
2025-08-26 10:38:53 -06:00
Danilo Leal
65c6c709fd thread view: Refine tool call UI (#36937)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-26 12:55:40 -03:00
Bennet Bo Fenner
858ab9cc23 Revert "ai: Auto select user model when there's no default" (#36932)
Reverts zed-industries/zed#36722

Release Notes:

- N/A
2025-08-26 13:55:09 +00:00
Daniel Martín
2c64b05ea4 emacs: Add editor::FindAllReferences keybinding (#36840)
This commit maps `editor::FindAllReferences` to Alt+? in the Emacs
keymap.

Release Notes:

- N/A
2025-08-26 13:43:58 +00:00
Peter Tripp
b7dad2cf71 Fix initial_tasks.json triggering diagnostic warning (#36523)
`zed::OpenProjectTasks` without an existing tasks.json will recreate it
from the template.
This file will immediately show a warning.

<img width="810" height="168" alt="Screenshot 2025-08-19 at 17 16 07"
src="https://github.com/user-attachments/assets/bbc8c7a0-7036-4927-8e85-b81b79aeaacb"
/>

Release Notes:

- N/A
2025-08-26 13:41:57 +00:00
Peter Tripp
76dbcde628 Support disabling drag-and-drop in Project Panel (#36719)
Release Notes:

- Added setting for disabling drag and drop in project panel. `{
"project_panel": {"drag_and_drop": false } }`
2025-08-26 13:35:45 +00:00
Peter Tripp
aa0f7a2d09 Fix conflicts in Linux default keymap (#36519)
Closes https://github.com/zed-industries/zed/issues/29746

| Action | New Key | Old Key | Former Conflict |
| - | - | - | - |
| `edit_prediction::ToggleMenu` | `ctrl-alt-shift-i` | `ctrl-shift-i` |
`editor::Format` |
| `editor::ToggleEditPrediction` | `ctrl-alt-shift-e` | `ctrl-shift-e` |
`project_panel::ToggleFocus` |

These aren't great keys and I'm open to alternate suggestions, but the
will work out of the box without conflict.

Release Notes:

- N/A
2025-08-26 09:33:42 -04:00
Bennet Bo Fenner
372b3c7af6 acp: Enable feature flag for everyone (#36928)
Release Notes:

- N/A
2025-08-26 15:30:26 +02:00
Bennet Bo Fenner
10a1140d49 acp: Improve matching logic when adding new entry to agent_servers (#36926)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-26 11:18:50 +00:00
Bennet Bo Fenner
e96b68bc15 acp: Polish UI (#36927)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-26 10:55:45 +00:00
Ben Brandt
b249593abe agent2: Always finalize diffs from the edit tool (#36918)
Previously, we wouldn't finalize the diff if an error occurred during
editing or the tool call was canceled.

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-26 09:46:29 +00:00
Bennet Bo Fenner
c14d84cfdb acp: Add button to configure custom agent in the configuration view (#36923)
Release Notes:

- N/A
2025-08-26 09:20:33 +00:00
Dan Dascalescu
428fc6d483 chore: Fix typo in 10_bug_report.yml (#36922)
Release Notes:

- N/A
2025-08-26 11:05:40 +02:00
Michael Sloan
829cffd37f Also record repo relative file path 2025-08-26 00:24:20 -06:00
Michael Sloan
b9cd8f5d2a Cleanup + only record git info if current file may be in repo 2025-08-26 00:22:41 -06:00
Max Brunsfeld
64b14ef848 Fix Sqlite newline syntax in workspace migration (#36916)
Fixes one more case where I incorrectly tried to use a `\n` escape
sequence for a newline in sqlite.

Release Notes:

- N/A
2025-08-25 22:21:05 -07:00
Michael Sloan
87609557f0 Filter out excessively long paths 2025-08-25 22:47:00 -06:00
Michael Sloan
ee6a8a20e2 Use worktree status and git status when filtering recent files list 2025-08-25 22:44:08 -06:00
Michael Sloan
b40794d413 Refactor Repository::status_for_path to return FileStatus instead of StatusEntry 2025-08-25 22:01:36 -06:00
Michael Sloan
b696a32518 zeta: Record recently active files when data collection is enabled 2025-08-25 21:57:15 -06:00
Rui Ning
bf5ed6d1c9 Remote: Change "sh -c" to "sh -lc" to make config in $HOME/.profile effective (#36760)
Closes #ISSUE

Release Notes:

- The environment of original remote dev cannot be changed without sudo
because of the behavior of "sh -c". This PR changes "sh -c" to "sh -lc"
to let the shell source $HOME/.profile and support customized
environment like customized $PATH variable.
2025-08-25 21:40:53 -06:00
Romans Malinovskis
bb5cfe118f Add "shift-r" and "g ." support for helix mode (#35468)
Related #4642
Compatible with #34136

Release Notes:

- Helix: `Shift+R` works as Paste instead of taking you to ReplaceMode
- Helix: `g .` goes to last modification place (similar to `. in vim)
2025-08-25 21:37:29 -06:00
Conrad Irwin
633ce23ae9 acp: Send user-configured MCP tools (#36910)
Release Notes:

- N/A
2025-08-26 00:55:24 +00:00
Max Brunsfeld
d43df9e841 Fix workspace migration failure (#36911)
This fixes a regression on nightly introduced in
https://github.com/zed-industries/zed/pull/36714

Release Notes:

- N/A
2025-08-26 00:27:52 +00:00
Conrad Irwin
f8667a8379 Remove unused files (#36909)
Closes #ISSUE

Release Notes:

- N/A
2025-08-25 22:23:58 +00:00
396 changed files with 24963 additions and 14754 deletions

View File

@@ -14,7 +14,7 @@ body:
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- Code must as text, not just as a screenshot.
- Include code as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->

View File

@@ -27,6 +27,22 @@ By effectively engaging with the Zed team and community early in your process, w
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
## Mandatory PR contents
Please ensure the PR contains
- Before & after screenshots, if there are visual adjustments introduced.
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
- A disclosure of the AI assistance usage, if any was used.
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
If the PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
## Tips to improve the chances of your PR getting reviewed and merged
- Discuss your plans ahead of time with the team

185
Cargo.lock generated
View File

@@ -8,6 +8,7 @@ version = "0.1.0"
dependencies = [
"action_log",
"agent-client-protocol",
"agent_settings",
"anyhow",
"buffer_diff",
"collections",
@@ -191,9 +192,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.0.31"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "289eb34ee17213dadcca47eedadd386a5e7678094095414e475965d1bcca2860"
checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a"
dependencies = [
"anyhow",
"async-broadcast",
@@ -292,23 +293,21 @@ dependencies = [
"anyhow",
"client",
"collections",
"context_server",
"env_logger 0.11.8",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"language_models",
"libc",
"log",
"nix 0.29.0",
"node_runtime",
"paths",
"project",
"rand 0.8.5",
"reqwest_client",
"schemars",
"semver",
@@ -316,12 +315,10 @@ dependencies = [
"serde_json",
"settings",
"smol",
"strum 0.27.1",
"tempfile",
"thiserror 2.0.12",
"ui",
"util",
"uuid",
"watch",
"which 6.0.3",
"workspace-hack",
@@ -418,6 +415,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"shlex",
"smol",
"streaming_diff",
"task",
@@ -509,7 +507,7 @@ dependencies = [
"parking_lot",
"piper",
"polling",
"regex-automata 0.4.9",
"regex-automata",
"rustix-openpty",
"serde",
"signal-hook",
@@ -2459,7 +2457,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"serde",
]
@@ -4734,7 +4732,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term 0.50.1",
"nu-ansi-term",
]
[[package]]
@@ -5633,8 +5631,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set 0.5.3",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -5644,8 +5642,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -7295,8 +7293,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -8301,7 +8299,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -8900,7 +8898,7 @@ dependencies = [
"percent-encoding",
"referencing",
"regex",
"regex-syntax 0.8.5",
"regex-syntax",
"reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
@@ -8953,6 +8951,44 @@ dependencies = [
"uuid",
]
[[package]]
name = "keymap_editor"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"component",
"db",
"editor",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -9213,6 +9249,7 @@ dependencies = [
"language",
"lsp",
"project",
"proto",
"release_channel",
"serde_json",
"settings",
@@ -9701,7 +9738,7 @@ dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax 0.8.5",
"regex-syntax",
"rustc_version",
"syn 2.0.101",
]
@@ -9773,7 +9810,7 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec"
source = "git+https://github.com/zed-industries/lsp-types?rev=0874f8742fe55b4dc94308c1e3c0069710d8eeaf#0874f8742fe55b4dc94308c1e3c0069710d8eeaf"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -9916,9 +9953,11 @@ dependencies = [
"editor",
"fs",
"gpui",
"html5ever 0.27.0",
"language",
"linkify",
"log",
"markup5ever_rcdom",
"pretty_assertions",
"pulldown-cmark 0.12.2",
"settings",
@@ -9979,11 +10018,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -10684,16 +10723,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -11387,12 +11416,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.11.1"
@@ -13383,17 +13406,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -13404,7 +13418,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
@@ -13413,12 +13427,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -14857,6 +14865,8 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_json_lenient",
"serde_path_to_error",
"settings_ui_macros",
"smallvec",
"tree-sitter",
"tree-sitter-json",
@@ -14892,39 +14902,28 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"command_palette_hooks",
"component",
"db",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"smallvec",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "settings_ui_macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"workspace-hack",
]
[[package]]
@@ -16740,6 +16739,7 @@ dependencies = [
"db",
"gpui",
"http_client",
"keymap_editor",
"notifications",
"pretty_assertions",
"project",
@@ -16748,7 +16748,6 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@@ -17117,14 +17116,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term 0.46.0",
"nu-ansi-term",
"once_cell",
"regex",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
@@ -17155,7 +17154,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
dependencies = [
"cc",
"regex",
"regex-syntax 0.8.5",
"regex-syntax",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
@@ -17185,8 +17184,7 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
dependencies = [
"cc",
"tree-sitter-language",
@@ -19954,8 +19952,8 @@ dependencies = [
"rand_core 0.6.4",
"regalloc2",
"regex",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
"ring",
"rust_decimal",
"rustc-hash 1.1.0",
@@ -20396,7 +20394,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.202.0"
version = "0.203.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20460,6 +20458,7 @@ dependencies = [
"itertools 0.14.0",
"jj_ui",
"journal",
"keymap_editor",
"language",
"language_extension",
"language_model",
@@ -20590,7 +20589,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -20789,10 +20788,12 @@ dependencies = [
"gpui",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"log",
"menu",
"multi_buffer",
"postage",
"project",
"rand 0.8.5",
@@ -20803,6 +20804,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"telemetry",
"telemetry_events",
"theme",
@@ -20810,7 +20812,6 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"uuid",
"workspace",

View File

@@ -54,6 +54,8 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -82,13 +84,12 @@ members = [
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
@@ -146,6 +147,7 @@ members = [
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -156,9 +158,9 @@ members = [
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
"crates/system_specs",
"crates/supermaven_api",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -314,6 +316,7 @@ install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
@@ -373,6 +376,7 @@ semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_ui = { path = "crates/settings_ui" }
settings_ui_macros = { path = "crates/settings_ui_macros" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
@@ -426,7 +430,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = "0.0.31"
agent-client-protocol = "0.1"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -519,7 +523,7 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
@@ -588,6 +592,7 @@ serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
@@ -624,7 +629,7 @@ tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
@@ -691,6 +696,7 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-filter-icon lucide-list-filter"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 336 B

1257
assets/images/acp_grid.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 176 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -40,7 +40,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
@@ -120,7 +120,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -130,8 +130,8 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
@@ -170,6 +170,7 @@
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
"ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -258,6 +259,7 @@
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char

View File

@@ -38,6 +38,7 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char

View File

@@ -354,6 +354,15 @@
"ctrl-s": "editor::ShowSignatureHelp"
}
},
{
"context": "showing_completions",
"bindings": {
"ctrl-d": "vim::ScrollDown",
"ctrl-u": "vim::ScrollUp",
"ctrl-e": "vim::LineDown",
"ctrl-y": "vim::LineUp"
}
},
{
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
@@ -428,12 +437,14 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
"g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
"x": "editor::SelectLine",
"shift-r": "editor::Paste",
"x": "vim::HelixSelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode

View File

@@ -223,9 +223,25 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// Whether the text selection should have rounded corners.
"rounded_selection": true,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// This only affects text drawn over highlight backgrounds in the editor.
"minimum_contrast_for_highlights": 45,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -279,6 +295,8 @@
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
// The default number of context lines shown in multibuffer excerpts.
"excerpt_context_lines": 2,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
@@ -363,6 +381,8 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
@@ -653,6 +673,8 @@
// "never"
"show": "always"
},
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
@@ -1581,7 +1603,7 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
@@ -1606,7 +1628,7 @@
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
@@ -1754,7 +1776,7 @@
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
"api_url": "https://api.deepseek.com"
"api_url": "https://api.deepseek.com/v1"
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
@@ -1902,7 +1924,10 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
"timeout": 2000,
"dock": "bottom",
"log_dap_communications": true,
"format_dap_log_messages": true,
"button": true
},
// Configures any number of settings profiles that are temporarily applied on

View File

@@ -43,8 +43,8 @@
// "args": ["--login"]
// }
// }
"shell": "system",
"shell": "system"
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
"tags": []
// "tags": []
}
]

View File

@@ -19,6 +19,7 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
action_log.workspace = true
agent-client-protocol.workspace = true
anyhow.workspace = true
agent_settings.workspace = true
buffer_diff.workspace = true
collections.workspace = true
editor.workspace = true

View File

@@ -3,6 +3,7 @@ mod diff;
mod mention;
mod terminal;
use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
pub use diff::*;
@@ -10,6 +11,7 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use settings::Settings as _;
pub use terminal::*;
use action_log::ActionLog;
@@ -789,16 +791,12 @@ pub enum ThreadStatus {
#[derive(Debug, Clone)]
pub enum LoadError {
NotInstalled {
error_message: SharedString,
install_message: SharedString,
install_command: String,
},
Unsupported {
error_message: SharedString,
upgrade_message: SharedString,
upgrade_command: String,
command: SharedString,
current_version: SharedString,
minimum_version: SharedString,
},
FailedToInstall(SharedString),
Exited {
status: ExitStatus,
},
@@ -808,12 +806,19 @@ pub enum LoadError {
impl Display for LoadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
LoadError::NotInstalled { error_message, .. }
| LoadError::Unsupported { error_message, .. } => {
write!(f, "{error_message}")
LoadError::Unsupported {
command: path,
current_version,
minimum_version,
} => {
write!(
f,
"version {current_version} from {path} is not supported (need at least {minimum_version})"
)
}
LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"),
LoadError::Exited { status } => write!(f, "Server exited with status {status}"),
LoadError::Other(msg) => write!(f, "{}", msg),
LoadError::Other(msg) => write!(f, "{msg}"),
}
}
}
@@ -1227,9 +1232,29 @@ impl AcpThread {
tool_call: acp::ToolCallUpdate,
options: Vec<acp::PermissionOption>,
cx: &mut Context<Self>,
) -> Result<oneshot::Receiver<acp::PermissionOptionId>, acp::Error> {
) -> Result<BoxFuture<'static, acp::RequestPermissionOutcome>> {
let (tx, rx) = oneshot::channel();
if AgentSettings::get_global(cx).always_allow_tool_actions {
// Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
// some tools would (incorrectly) continue to auto-accept.
if let Some(allow_once_option) = options.iter().find_map(|option| {
if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
Some(option.id.clone())
} else {
None
}
}) {
self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?;
return Ok(async {
acp::RequestPermissionOutcome::Selected {
option_id: allow_once_option,
}
}
.boxed());
}
}
let status = ToolCallStatus::WaitingForConfirmation {
options,
respond_tx: tx,
@@ -1237,7 +1262,16 @@ impl AcpThread {
self.upsert_tool_call_inner(tool_call, status, cx)?;
cx.emit(AcpThreadEvent::ToolAuthorizationRequired);
Ok(rx)
let fut = async {
match rx.await {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
}
}
.boxed();
Ok(fut)
}
pub fn authorize_tool_call(

View File

@@ -393,14 +393,15 @@ mod test_support {
};
let task = cx.spawn(async move |cx| {
if let Some((tool_call, options)) = permission_request {
let permission = thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
cx,
)
})?;
permission?.await?;
thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
cx,
)
})??
.await;
}
thread.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{MultiBuffer, PathKey};
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
use itertools::Itertools;
use language::{
@@ -64,7 +64,7 @@ impl Diff {
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(diff, cx);
@@ -279,7 +279,7 @@ impl PendingDiff {
path_key,
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -305,7 +305,7 @@ impl PendingDiff {
PathKey::for_buffer(&self.new_buffer, cx),
self.new_buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);

View File

@@ -664,7 +664,7 @@ impl Thread {
}
pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option<ConfiguredModel> {
if self.configured_model.is_none() || self.messages.is_empty() {
if self.configured_model.is_none() {
self.configured_model = LanguageModelRegistry::read_global(cx).default_model();
}
self.configured_model.clone()
@@ -2097,7 +2097,7 @@ impl Thread {
}
pub fn summarize(&mut self, cx: &mut Context<Self>) {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else {
let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else {
println!("No thread summary model");
return;
};
@@ -2416,7 +2416,7 @@ impl Thread {
}
let Some(ConfiguredModel { model, provider }) =
LanguageModelRegistry::read_global(cx).thread_summary_model(cx)
LanguageModelRegistry::read_global(cx).thread_summary_model()
else {
return;
};
@@ -5410,10 +5410,13 @@ fn main() {{
}),
cx,
);
registry.set_thread_summary_model(Some(ConfiguredModel {
provider,
model: model.clone(),
}));
registry.set_thread_summary_model(
Some(ConfiguredModel {
provider,
model: model.clone(),
}),
cx,
);
})
});

View File

@@ -61,16 +61,19 @@ pub struct LanguageModels {
model_list: acp_thread::AgentModelList,
refresh_models_rx: watch::Receiver<()>,
refresh_models_tx: watch::Sender<()>,
_authenticate_all_providers_task: Task<()>,
}
impl LanguageModels {
fn new(cx: &App) -> Self {
fn new(cx: &mut App) -> Self {
let (refresh_models_tx, refresh_models_rx) = watch::channel(());
let mut this = Self {
models: HashMap::default(),
model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
refresh_models_rx,
refresh_models_tx,
_authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx),
};
this.refresh_list(cx);
this
@@ -90,7 +93,7 @@ impl LanguageModels {
let mut recommended = Vec::new();
for provider in &providers {
for model in provider.recommended_models(cx) {
recommended_models.insert(model.id());
recommended_models.insert((model.provider_id(), model.id()));
recommended.push(Self::map_language_model_to_info(&model, provider));
}
}
@@ -107,7 +110,7 @@ impl LanguageModels {
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
if !recommended_models.contains(&model.id()) {
if !recommended_models.contains(&(model.provider_id(), model.id())) {
provider_models.push(model_info);
}
models.insert(model_id, model);
@@ -150,6 +153,52 @@ impl LanguageModels {
fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
}
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
cx.background_spawn(async move {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, language_model::AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}
})
}
}
pub struct NativeAgent {
@@ -228,7 +277,7 @@ impl NativeAgent {
) -> Entity<AcpThread> {
let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model(cx).map(|c| c.model);
let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
@@ -524,7 +573,7 @@ impl NativeAgent {
let registry = LanguageModelRegistry::read_global(cx);
let default_model = registry.default_model().map(|m| m.model);
let summarization_model = registry.thread_summary_model(cx).map(|m| m.model);
let summarization_model = registry.thread_summary_model().map(|m| m.model);
for session in self.sessions.values_mut() {
session.thread.update(cx, |thread, cx| {
@@ -713,18 +762,15 @@ impl NativeAgentConnection {
options,
response,
}) => {
let recv = acp_thread.update(cx, |thread, cx| {
let outcome_task = acp_thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call, options, cx)
})?;
})??;
cx.background_spawn(async move {
if let Some(recv) = recv.log_err()
&& let Some(option) = recv
.await
.context("authorization sender was dropped")
.log_err()
if let acp::RequestPermissionOutcome::Selected { option_id } =
outcome_task.await
{
response
.send(option)
.send(option_id)
.map(|_| anyhow!("authorization receiver was dropped"))
.log_err();
}

View File

@@ -1,10 +1,9 @@
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_servers::AgentServer;
use agent_servers::{AgentServer, AgentServerDelegate};
use anyhow::Result;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use prompt_store::PromptStore;
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
@@ -30,14 +29,6 @@ impl AgentServer for NativeAgentServer {
"Zed Agent".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::ZedAgent
}
@@ -45,14 +36,14 @@ impl AgentServer for NativeAgentServer {
fn connect(
&self,
_root_dir: &Path,
project: &Entity<Project>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
);
let project = project.clone();
let project = delegate.project().clone();
let fs = self.fs.clone();
let history = self.history.clone();
let prompt_store = PromptStore::global(cx);

View File

@@ -72,6 +72,7 @@ async fn test_echo(cx: &mut TestAppContext) {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_thinking(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -471,7 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
tool_name: ToolRequiringPermission::name().into(),
is_error: true,
content: "Permission to run tool denied by user".into(),
output: None
output: Some("Permission to run tool denied by user".into())
})
]
);
@@ -949,6 +950,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
paths::settings_file(),
json!({
"agent": {
"always_allow_tool_actions": true,
"profiles": {
"test": {
"name": "Test Profile",
@@ -1347,6 +1349,7 @@ async fn test_cancellation(cx: &mut TestAppContext) {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -1820,11 +1823,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
let clock = Arc::new(clock::FakeSystemClock::new());
let client = Client::new(clock, http_client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
Project::init_settings(cx);
agent_settings::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
Project::init_settings(cx);
LanguageModelRegistry::test(cx);
agent_settings::init(cx);
});
cx.executor().forbid_parking();

View File

@@ -484,11 +484,15 @@ impl AgentMessage {
};
for tool_result in self.tool_results.values() {
let mut tool_result = tool_result.clone();
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
if tool_result.content.is_empty() {
tool_result.content = "<Tool returned an empty string>".into();
}
user_message
.content
.push(language_model::MessageContent::ToolResult(
tool_result.clone(),
));
.push(language_model::MessageContent::ToolResult(tool_result));
}
let mut messages = Vec::new();
@@ -732,7 +736,17 @@ impl Thread {
stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
status: Some(
tool_result
.as_ref()
.map_or(acp::ToolCallStatus::Failed, |result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
}),
),
raw_output: output,
..Default::default()
},
@@ -1557,7 +1571,7 @@ impl Thread {
tool_name: tool_use.name,
is_error: true,
content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())),
output: None,
output: Some(error.to_string().into()),
},
}
}))
@@ -2459,6 +2473,30 @@ impl ToolCallEventStreamReceiver {
}
}
pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
update,
)))) = event
{
update.fields
} else {
panic!("Expected update fields but got: {:?}", event);
}
}
pub async fn expect_diff(&mut self) -> Entity<acp_thread::Diff> {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff(
update,
)))) = event
{
update.diff
} else {
panic!("Expected diff but got: {:?}", event);
}
}
pub async fn expect_terminal(&mut self) -> Entity<acp_thread::Terminal> {
let event = self.0.next().await;
if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal(

View File

@@ -169,15 +169,18 @@ impl AnyAgentTool for ContextServerTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_event_stream: ToolCallEventStream,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<AgentToolOutput>> {
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
let authorize = event_stream.authorize(self.initial_title(input.clone()), cx);
cx.spawn(async move |_cx| {
authorize.await?;
let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};

View File

@@ -273,6 +273,13 @@ impl AgentTool for EditFileTool {
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
let diff = diff.downgrade();
let mut cx = cx.clone();
move || {
diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok();
}
});
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
@@ -389,8 +396,6 @@ impl AgentTool for EditFileTool {
})
.await;
diff.update(cx, |diff, cx| diff.finalize(cx)).ok();
let input_path = input.path.display();
if unified_diff.is_empty() {
anyhow::ensure!(
@@ -1545,6 +1550,100 @@ mod tests {
);
}
#[gpui::test]
async fn test_diff_finalization(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/", json!({"main.rs": ""})).await;
let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await;
let languages = project.read_with(cx, |project, _cx| project.languages().clone());
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry.clone(),
Templates::new(),
Some(model.clone()),
cx,
)
});
// Ensure the diff is finalized after the edit completes.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
cx.run_until_parked();
model.end_last_completion_stream();
edit.await.unwrap();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
// Ensure the diff is finalized if an error occurs while editing.
{
model.forbid_requests();
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
edit.await.unwrap_err();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
model.allow_requests();
}
// Ensure the diff is finalized if the tool call gets dropped.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
EditFileToolInput {
display_description: "Edit file".into(),
path: path!("/main.rs").into(),
mode: EditFileMode::Edit,
},
stream_tx,
cx,
)
});
stream_rx.expect_update_fields().await;
let diff = stream_rx.expect_diff().await;
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_))));
drop(edit);
cx.run_until_parked();
diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_))));
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -2,7 +2,7 @@ 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 project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
@@ -144,14 +144,14 @@ impl AgentTool for TerminalTool {
let terminal = self
.project
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(program),
args,
cwd: working_dir.clone(),
env,
..Default::default()
}),
},
cx,
)
})?

View File

@@ -6,7 +6,7 @@ publish.workspace = true
license = "GPL-3.0-or-later"
[features]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "fs", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"]
e2e = []
[lints]
@@ -25,21 +25,19 @@ agent_settings.workspace = true
anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true
context_server.workspace = true
env_logger = { workspace = true, optional = true }
fs = { workspace = true, optional = true }
fs.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true
rand.workspace = true
reqwest_client = { workspace = true, optional = true }
schemars.workspace = true
semver.workspace = true
@@ -47,12 +45,10 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strum.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -6,10 +6,10 @@ use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error;
@@ -30,6 +30,8 @@ pub struct AcpConnection {
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
}
pub struct AcpSession {
@@ -56,7 +58,7 @@ impl AcpConnection {
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(&command.path)
let mut child = util::command::new_smol_command(command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
@@ -86,7 +88,7 @@ impl AcpConnection {
let io_task = cx.background_spawn(io_task);
cx.background_spawn(async move {
let stderr_task = cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
@@ -95,10 +97,10 @@ impl AcpConnection {
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
Ok(())
});
cx.spawn({
let wait_task = cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
@@ -114,8 +116,7 @@ impl AcpConnection {
anyhow::Ok(())
}
})
.detach();
});
let connection = Rc::new(connection);
@@ -148,8 +149,14 @@ impl AcpConnection {
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
})
}
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
&self.prompt_capabilities
}
}
impl AgentConnection for AcpConnection {
@@ -162,12 +169,34 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
})
.collect();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest {
mcp_servers: vec![],
cwd,
})
.new_session(acp::NewSessionRequest { mcp_servers, cwd })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -313,7 +342,8 @@ impl acp::Client for ClientDelegate {
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
let rx = self
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
@@ -321,14 +351,9 @@ impl acp::Client for ClientDelegate {
.thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?;
})??;
let result = rx?.await;
let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
};
let outcome = task.await;
Ok(acp::RequestPermissionResponse { outcome })
}

View File

@@ -1,524 +0,0 @@
// Translates old acp agents into the new schema
use action_log::ActionLog;
use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
use anyhow::{Context as _, Result, anyhow};
use futures::channel::oneshot;
use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use std::{any::Any, cell::RefCell, path::Path, rc::Rc};
use ui::App;
use util::ResultExt as _;
use crate::AgentServerCommand;
use acp_thread::{AcpThread, AgentConnection, AuthRequired};
#[derive(Clone)]
struct OldAcpClientDelegate {
thread: Rc<RefCell<WeakEntity<AcpThread>>>,
cx: AsyncApp,
next_tool_call_id: Rc<RefCell<u64>>,
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
}
impl OldAcpClientDelegate {
fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
Self {
thread,
cx,
next_tool_call_id: Rc::new(RefCell::new(0)),
}
}
}
impl acp_old::Client for OldAcpClientDelegate {
async fn stream_assistant_message_chunk(
&self,
params: acp_old::StreamAssistantMessageChunkParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| match params.chunk {
acp_old::AssistantMessageChunk::Text { text } => {
thread.push_assistant_content_block(text.into(), false, cx)
}
acp_old::AssistantMessageChunk::Thought { thought } => {
thread.push_assistant_content_block(thought.into(), true, cx)
}
})
.log_err();
})?;
Ok(())
}
async fn request_tool_call_confirmation(
&self,
request: acp_old::RequestToolCallConfirmationParams,
) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
let tool_call = into_new_tool_call(
acp::ToolCallId(old_acp_id.to_string().into()),
request.tool_call,
);
let mut options = match request.confirmation {
acp_old::ToolCallConfirmation::Edit { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow Edits".to_string(),
)],
acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", root_command),
)],
acp_old::ToolCallConfirmation::Mcp {
server_name,
tool_name,
..
} => vec![
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", server_name),
),
(
acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
acp::PermissionOptionKind::AllowAlways,
format!("Always Allow {}", tool_name),
),
],
acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
acp_old::ToolCallConfirmation::Other { .. } => vec![(
acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
acp::PermissionOptionKind::AllowAlways,
"Always Allow".to_string(),
)],
};
options.extend([
(
acp_old::ToolCallConfirmationOutcome::Allow,
acp::PermissionOptionKind::AllowOnce,
"Allow".to_string(),
),
(
acp_old::ToolCallConfirmationOutcome::Reject,
acp::PermissionOptionKind::RejectOnce,
"Reject".to_string(),
),
]);
let mut outcomes = Vec::with_capacity(options.len());
let mut acp_options = Vec::with_capacity(options.len());
for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
outcomes.push(outcome);
acp_options.push(acp::PermissionOption {
id: acp::PermissionOptionId(index.to_string().into()),
name: label,
kind,
})
}
let response = cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call.into(), acp_options, cx)
})
})??
.context("Failed to update thread")?
.await;
let outcome = match response {
Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
};
Ok(acp_old::RequestToolCallConfirmationResponse {
id: acp_old::ToolCallId(old_acp_id),
outcome,
})
}
async fn push_tool_call(
&self,
request: acp_old::PushToolCallParams,
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
let cx = &mut self.cx.clone();
let old_acp_id = *self.next_tool_call_id.borrow() + 1;
self.next_tool_call_id.replace(old_acp_id);
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.upsert_tool_call(
into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
cx,
)
})
})??
.context("Failed to update thread")?;
Ok(acp_old::PushToolCallResponse {
id: acp_old::ToolCallId(old_acp_id),
})
}
async fn update_tool_call(
&self,
request: acp_old::UpdateToolCallParams,
) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallUpdate {
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
fields: acp::ToolCallUpdateFields {
status: Some(into_new_tool_call_status(request.status)),
content: Some(
request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect::<Vec<_>>(),
),
..Default::default()
},
},
cx,
)
})
})?
.context("Failed to update thread")??;
Ok(())
}
async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.update_plan(
acp::Plan {
entries: request
.entries
.into_iter()
.map(into_new_plan_entry)
.collect(),
},
cx,
)
})
})?
.context("Failed to update thread")?;
Ok(())
}
async fn read_text_file(
&self,
acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
let content = self
.cx
.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
thread.read_text_file(path, line, limit, false, cx)
})
})?
.context("Failed to update thread")?
.await?;
Ok(acp_old::ReadTextFileResponse { content })
}
async fn write_text_file(
&self,
acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
) -> Result<(), acp_old::Error> {
self.cx
.update(|cx| {
self.thread
.borrow()
.update(cx, |thread, cx| thread.write_text_file(path, content, cx))
})?
.context("Failed to update thread")?
.await?;
Ok(())
}
}
fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
acp::ToolCall {
id,
title: request.label,
kind: acp_kind_from_old_icon(request.icon),
status: acp::ToolCallStatus::InProgress,
content: request
.content
.into_iter()
.map(into_new_tool_call_content)
.collect(),
locations: request
.locations
.into_iter()
.map(into_new_tool_call_location)
.collect(),
raw_input: None,
raw_output: None,
}
}
fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
match icon {
acp_old::Icon::FileSearch => acp::ToolKind::Search,
acp_old::Icon::Folder => acp::ToolKind::Search,
acp_old::Icon::Globe => acp::ToolKind::Search,
acp_old::Icon::Hammer => acp::ToolKind::Other,
acp_old::Icon::LightBulb => acp::ToolKind::Think,
acp_old::Icon::Pencil => acp::ToolKind::Edit,
acp_old::Icon::Regex => acp::ToolKind::Search,
acp_old::Icon::Terminal => acp::ToolKind::Execute,
}
}
fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
match status {
acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
}
}
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
match content {
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
diff: into_new_diff(diff),
},
}
}
fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
acp::Diff {
path: diff.path,
old_text: diff.old_text,
new_text: diff.new_text,
}
}
fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
acp::ToolCallLocation {
path: location.path,
line: location.line,
}
}
fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
acp::PlanEntry {
content: entry.content,
priority: into_new_plan_priority(entry.priority),
status: into_new_plan_status(entry.status),
}
}
fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
match priority {
acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
}
}
fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
match status {
acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
pub struct AcpConnection {
pub name: &'static str,
pub connection: acp_old::AgentConnection,
pub _child_status: Task<Result<()>>,
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
}
impl AcpConnection {
pub fn stdio(
name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Self>> {
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |cx| {
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit())
.kill_on_drop(true)
.spawn()?;
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
log::trace!("Spawned (pid: {})", child.id());
let foreground_executor = cx.foreground_executor().clone();
let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
stdin,
stdout,
move |fut| foreground_executor.spawn(fut).detach(),
);
let io_task = cx.background_spawn(async move {
io_fut.await.log_err();
});
let child_status = cx.background_spawn(async move {
let result = match child.status().await {
Err(e) => Err(anyhow!(e)),
Ok(result) if result.success() => Ok(()),
Ok(result) => Err(anyhow!(result)),
};
drop(io_task);
result
});
Ok(Self {
name,
connection,
_child_status: child_status,
current_thread: thread_rc,
})
})
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let task = self.connection.request_any(
acp_old::InitializeParams {
protocol_version: acp_old::ProtocolVersion::latest(),
}
.into_any(),
);
let current_thread = self.current_thread.clone();
cx.spawn(async move |cx| {
let result = task.await?;
let result = acp_old::InitializeParams::response_from_any(result)?;
if !result.is_authenticated {
anyhow::bail!(AuthRequired::new())
}
cx.update(|cx| {
let thread = cx.new(|cx| {
let session_id = acp::SessionId("acp-old-no-id".into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
AcpThread::new(self.name, self.clone(), project, action_log, session_id)
});
current_thread.replace(thread.downgrade());
thread
})
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}
fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let task = self
.connection
.request_any(acp_old::AuthenticateParams.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
Ok(())
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let chunks = params
.prompt
.into_iter()
.filter_map(|block| match block {
acp::ContentBlock::Text(text) => {
Some(acp_old::UserMessageChunk::Text { text: text.text })
}
acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
path: link.uri.into(),
}),
_ => None,
})
.collect();
let task = self
.connection
.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
cx.foreground_executor().spawn(async move {
task.await?;
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
})
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
acp::PromptCapabilities {
image: false,
audio: false,
embedded_context: false,
}
}
fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
let task = self
.connection
.request_any(acp_old::CancelSendMessageParams.into_any());
cx.foreground_executor()
.spawn(async move {
task.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}

View File

@@ -1,376 +0,0 @@
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use serde::Deserialize;
use std::path::Path;
use std::rc::Rc;
use std::{any::Any, cell::RefCell};
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use crate::{AgentServerCommand, acp::UnsupportedVersion};
use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError};
pub struct AcpConnection {
server_name: &'static str,
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
_io_task: Task<Result<()>>,
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
}
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
server_name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(&command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
let stderr = child.stderr.take().context("Failed to take stderr")?;
log::trace!("Spawned (pid: {})", child.id());
let sessions = Rc::new(RefCell::new(HashMap::default()));
let client = ClientDelegate {
sessions: sessions.clone(),
cx: cx.clone(),
};
let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
let foreground_executor = cx.foreground_executor().clone();
move |fut| {
foreground_executor.spawn(fut).detach();
}
});
let io_task = cx.background_spawn(io_task);
cx.background_spawn(async move {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
line.clear();
}
})
.detach();
cx.spawn({
let sessions = sessions.clone();
async move |cx| {
let status = child.status().await?;
for session in sessions.borrow().values() {
session
.thread
.update(cx, |thread, cx| {
thread.emit_load_error(LoadError::Exited { status }, cx)
})
.ok();
}
anyhow::Ok(())
}
})
.detach();
let connection = Rc::new(connection);
cx.update(|cx| {
AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
registry.set_active_connection(server_name, &connection, cx)
});
})?;
let response = connection
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
},
},
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
return Err(UnsupportedVersion.into());
}
Ok(Self {
auth_methods: response.auth_methods,
connection,
server_name,
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
_io_task: io_task,
})
}
}
impl AgentConnection for AcpConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest {
mcp_servers: vec![],
cwd,
})
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
let mut error = AuthRequired::new();
if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
error = error.with_description(err.message);
}
anyhow!(error)
} else {
anyhow!(err)
}
})?;
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|_cx| {
AcpThread::new(
self.server_name,
self.clone(),
project,
action_log,
session_id.clone(),
)
})?;
let session = AcpSession {
thread: thread.downgrade(),
suppress_abort_err: false,
};
sessions.borrow_mut().insert(session_id, session);
Ok(thread)
})
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods
}
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
let result = conn
.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
})
.await?;
Ok(result)
})
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
params: acp::PromptRequest,
cx: &mut App,
) -> Task<Result<acp::PromptResponse>> {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let session_id = params.session_id.clone();
cx.foreground_executor().spawn(async move {
let result = conn.prompt(params).await;
let mut suppress_abort_err = false;
if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
suppress_abort_err = session.suppress_abort_err;
session.suppress_abort_err = false;
}
match result {
Ok(response) => Ok(response),
Err(err) => {
if err.code != ErrorCode::INTERNAL_ERROR.code {
anyhow::bail!(err)
}
let Some(data) = &err.data else {
anyhow::bail!(err)
};
// Temporary workaround until the following PR is generally available:
// https://github.com/google-gemini/gemini-cli/pull/6656
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ErrorDetails {
details: Box<str>,
}
match serde_json::from_value(data.clone()) {
Ok(ErrorDetails { details }) => {
if suppress_abort_err && details.contains("This operation was aborted")
{
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
})
} else {
Err(anyhow!(details))
}
}
Err(_) => Err(anyhow!(err)),
}
}
}
})
}
fn prompt_capabilities(&self) -> acp::PromptCapabilities {
self.prompt_capabilities
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
session.suppress_abort_err = true;
}
let conn = self.connection.clone();
let params = acp::CancelNotification {
session_id: session_id.clone(),
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
}
impl acp::Client for ClientDelegate {
async fn request_permission(
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
let rx = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?;
let result = rx?.await;
let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
};
Ok(acp::RequestPermissionResponse { outcome })
}
async fn write_text_file(
&self,
arguments: acp::WriteTextFileRequest,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
task.await?;
Ok(())
}
async fn read_text_file(
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
})?;
let content = task.await?;
Ok(acp::ReadTextFileResponse { content })
}
async fn session_notification(
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
}

View File

@@ -7,18 +7,29 @@ mod settings;
#[cfg(any(test, feature = "test-support"))]
pub mod e2e_tests;
use anyhow::Context as _;
pub use claude::*;
pub use custom::*;
use fs::Fs;
use fs::RemoveOptions;
use fs::RenameOptions;
use futures::StreamExt as _;
pub use gemini::*;
use gpui::AppContext;
use node_runtime::NodeRuntime;
pub use settings::*;
use acp_thread::AgentConnection;
use acp_thread::LoadError;
use anyhow::Result;
use anyhow::anyhow;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::str::FromStr as _;
use std::{
any::Any,
path::{Path, PathBuf},
@@ -31,17 +42,201 @@ pub fn init(cx: &mut App) {
settings::init(cx);
}
pub struct AgentServerDelegate {
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
}
impl AgentServerDelegate {
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
Self { project, status_tx }
}
pub fn project(&self) -> &Entity<Project> {
&self.project
}
fn get_or_npm_install_builtin_agent(
self,
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
ignore_system_version: bool,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
let project = self.project;
let fs = project.read(cx).fs().clone();
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
return Task::ready(Err(anyhow!(
"External agents are not yet available in remote projects."
)));
};
let status_tx = self.status_tx;
cx.spawn(async move |cx| {
if !ignore_system_version {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand {
path: bin,
args: Vec::new(),
env: Default::default(),
});
}
}
cx.spawn(async move |cx| {
let node_path = node_runtime.binary_path().await?;
let dir = paths::data_dir()
.join("external_agents")
.join(binary_name.as_str());
fs.create_dir(&dir).await?;
let mut stream = fs.read_dir(&dir).await?;
let mut versions = Vec::new();
let mut to_delete = Vec::new();
while let Some(entry) = stream.next().await {
let Ok(entry) = entry else { continue };
let Some(file_name) = entry.file_name() else {
continue;
};
if let Some(version) = file_name
.to_str()
.and_then(|name| semver::Version::from_str(&name).ok())
{
versions.push((version, file_name.to_owned()));
} else {
to_delete.push(file_name.to_owned())
}
}
versions.sort();
let newest_version = if let Some((version, file_name)) = versions.last().cloned()
&& minimum_version.is_none_or(|minimum_version| version >= minimum_version)
{
versions.pop();
Some(file_name)
} else {
None
};
log::debug!("existing version of {package_name}: {newest_version:?}");
to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
cx.background_spawn({
let fs = fs.clone();
let dir = dir.clone();
async move {
for file_name in to_delete {
fs.remove_dir(
&dir.join(file_name),
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await
.ok();
}
}
})
.detach();
let version = if let Some(file_name) = newest_version {
cx.background_spawn({
let file_name = file_name.clone();
let dir = dir.clone();
async move {
let latest_version =
node_runtime.npm_package_latest_version(&package_name).await;
if let Ok(latest_version) = latest_version
&& &latest_version != &file_name.to_string_lossy()
{
Self::download_latest_version(
fs,
dir.clone(),
node_runtime,
package_name,
)
.await
.log_err();
}
}
})
.detach();
file_name
} else {
if let Some(mut status_tx) = status_tx {
status_tx.send("Installing…".into()).ok();
}
let dir = dir.clone();
cx.background_spawn(Self::download_latest_version(
fs,
dir.clone(),
node_runtime,
package_name,
))
.await?
.into()
};
anyhow::Ok(AgentServerCommand {
path: node_path,
args: vec![
dir.join(version)
.join(entrypoint_path)
.to_string_lossy()
.to_string(),
],
env: Default::default(),
})
})
.await
.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
})
}
async fn download_latest_version(
fs: Arc<dyn Fs>,
dir: PathBuf,
node_runtime: NodeRuntime,
package_name: SharedString,
) -> Result<String> {
log::debug!("downloading latest version of {package_name}");
let tmp_dir = tempfile::tempdir_in(&dir)?;
node_runtime
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
.await?;
let version = node_runtime
.npm_package_installed_version(tmp_dir.path(), &package_name)
.await?
.context("expected package to be installed")?;
fs.rename(
&tmp_dir.keep(),
&dir.join(&version),
RenameOptions {
ignore_if_exists: true,
overwrite: false,
},
)
.await?;
anyhow::Ok(version)
}
}
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn empty_state_headline(&self) -> SharedString;
fn empty_state_message(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn connect(
&self,
root_dir: &Path,
project: &Entity<Project>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>>;
@@ -79,15 +274,6 @@ impl std::fmt::Debug for AgentServerCommand {
}
}
pub enum AgentServerVersion {
Supported,
Unsupported {
error_message: SharedString,
upgrade_message: SharedString,
upgrade_command: String,
},
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
@@ -102,23 +288,16 @@ impl AgentServerCommand {
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
settings: Option<AgentServerSettings>,
settings: Option<BuiltinAgentServerSettings>,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<Self> {
if let Some(agent_settings) = settings {
Some(Self {
path: agent_settings.command.path,
args: agent_settings
.command
.args
.into_iter()
.chain(extra_args.iter().map(|arg| arg.to_string()))
.collect(),
env: agent_settings.command.env,
})
if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
Some(command)
} else {
match find_bin_in_path(path_bin_name, project, cx).await {
match find_bin_in_path(path_bin_name.into(), project, cx).await {
Some(path) => Some(Self {
path,
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
@@ -141,7 +320,7 @@ impl AgentServerCommand {
}
async fn find_bin_in_path(
bin_name: &'static str,
bin_name: SharedString,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
@@ -171,11 +350,11 @@ async fn find_bin_in_path(
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name)
which::which(bin_name.as_str())
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref())
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {

File diff suppressed because it is too large Load Diff

View File

@@ -1,178 +0,0 @@
use acp_thread::AcpThread;
use anyhow::Result;
use context_server::{
listener::{McpServerTool, ToolResponse},
types::{ToolAnnotations, ToolResponseContent},
};
use gpui::{AsyncApp, WeakEntity};
use language::unified_diff;
use util::markdown::MarkdownCodeBlock;
use crate::tools::EditToolParams;
#[derive(Clone)]
pub struct EditTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl EditTool {
pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { thread_rx }
}
}
impl McpServerTool for EditTool {
type Input = EditToolParams;
type Output = ();
const NAME: &'static str = "Edit";
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Edit file".to_string()),
read_only_hint: Some(false),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: Some(false),
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
})?
.await?;
let (new_content, diff) = cx
.background_executor()
.spawn(async move {
let new_content = content.replace(&input.old_text, &input.new_text);
if new_content == content {
return Err(anyhow::anyhow!("Failed to find `old_text`",));
}
let diff = unified_diff(&content, &new_content);
Ok((new_content, diff))
})
.await?;
thread
.update(cx, |thread, cx| {
thread.write_text_file(input.abs_path, new_content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: MarkdownCodeBlock {
tag: "diff",
text: diff.as_str().trim_end_matches('\n'),
}
.to_string(),
}],
structured_content: (),
})
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use acp_thread::{AgentConnection, StubAgentConnection};
use gpui::{Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use util::path;
use super::*;
#[gpui::test]
async fn old_text_not_found(cx: &mut TestAppContext) {
let (_thread, tool) = init_test(cx).await;
let result = tool
.run(
EditToolParams {
abs_path: path!("/root/file.txt").into(),
old_text: "hi".into(),
new_text: "bye".into(),
},
&mut cx.to_async(),
)
.await;
assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`");
}
#[gpui::test]
async fn found_and_replaced(cx: &mut TestAppContext) {
let (_thread, tool) = init_test(cx).await;
let result = tool
.run(
EditToolParams {
abs_path: path!("/root/file.txt").into(),
old_text: "hello".into(),
new_text: "hi".into(),
},
&mut cx.to_async(),
)
.await;
assert_eq!(
result.unwrap().content[0].text().unwrap(),
indoc! {
r"
```diff
@@ -1,1 +1,1 @@
-hello
+hi
```
"
}
);
}
async fn init_test(cx: &mut TestAppContext) -> (Entity<AcpThread>, EditTool) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
let connection = Rc::new(StubAgentConnection::new());
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"file.txt": "hello"
}),
)
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
let thread = cx
.update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx))
.await
.unwrap();
thread_tx.send(thread.downgrade()).unwrap();
(thread, EditTool::new(thread_rx))
}
}

View File

@@ -1,99 +0,0 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::claude::edit_tool::EditTool;
use crate::claude::permission_tool::PermissionTool;
use crate::claude::read_tool::ReadTool;
use crate::claude::write_tool::WriteTool;
use acp_thread::AcpThread;
#[cfg(not(test))]
use anyhow::Context as _;
use anyhow::Result;
use collections::HashMap;
use context_server::types::{
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
ToolsCapabilities, requests,
};
use gpui::{App, AsyncApp, Task, WeakEntity};
use project::Fs;
use serde::Serialize;
pub struct ClaudeZedMcpServer {
server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
impl ClaudeZedMcpServer {
pub async fn new(
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
fs: Arc<dyn Fs>,
cx: &AsyncApp,
) -> Result<Self> {
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone()));
mcp_server.add_tool(ReadTool::new(thread_rx.clone()));
mcp_server.add_tool(EditTool::new(thread_rx.clone()));
mcp_server.add_tool(WriteTool::new(thread_rx.clone()));
Ok(Self { server: mcp_server })
}
pub fn server_config(&self) -> Result<McpServerConfig> {
#[cfg(not(test))]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in mcp_server")?;
#[cfg(test)]
let zed_path = crate::e2e_tests::get_zed_path();
Ok(McpServerConfig {
command: zed_path,
args: vec![
"--nc".into(),
self.server.socket_path().display().to_string(),
],
env: None,
})
}
fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
cx.foreground_executor().spawn(async move {
Ok(InitializeResponse {
protocol_version: ProtocolVersion("2025-06-18".into()),
capabilities: ServerCapabilities {
experimental: None,
logging: None,
completions: None,
prompts: None,
resources: None,
tools: Some(ToolsCapabilities {
list_changed: Some(false),
}),
},
server_info: Implementation {
name: SERVER_NAME.into(),
version: "0.1.0".into(),
},
meta: None,
})
})
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpConfig {
pub mcp_servers: HashMap<String, McpServerConfig>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub command: PathBuf,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
}

View File

@@ -1,158 +0,0 @@
use std::sync::Arc;
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use context_server::{
listener::{McpServerTool, ToolResponse},
types::ToolResponseContent,
};
use gpui::{AsyncApp, WeakEntity};
use project::Fs;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings as _, update_settings_file};
use util::debug_panic;
use crate::tools::ClaudeTool;
#[derive(Clone)]
pub struct PermissionTool {
fs: Arc<dyn Fs>,
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
/// Request permission for tool calls
#[derive(Deserialize, JsonSchema, Debug)]
pub struct PermissionToolParams {
tool_name: String,
input: serde_json::Value,
tool_use_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionToolResponse {
behavior: PermissionToolBehavior,
updated_input: serde_json::Value,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum PermissionToolBehavior {
Allow,
Deny,
}
impl PermissionTool {
pub fn new(fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { fs, thread_rx }
}
}
impl McpServerTool for PermissionTool {
type Input = PermissionToolParams;
type Output = ();
const NAME: &'static str = "Confirmation";
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
if agent_settings::AgentSettings::try_read_global(cx, |settings| {
settings.always_allow_tool_actions
})
.unwrap_or(false)
{
let response = PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
};
return Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
});
}
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
const ALWAYS_ALLOW: &str = "always_allow";
const ALLOW: &str = "allow";
const REJECT: &str = "reject";
let chosen_option = thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
claude_tool.as_acp(tool_call_id).into(),
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(REJECT.into()),
name: "Reject".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
cx,
)
})??
.await?;
let response = match chosen_option.0.as_ref() {
ALWAYS_ALLOW => {
cx.update(|cx| {
update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| {
settings.set_always_allow_tool_actions(true);
});
})?;
PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
}
}
ALLOW => PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
},
REJECT => PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
},
opt => {
debug_panic!("Unexpected option: {}", opt);
PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
}
}
};
Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
})
}
}

View File

@@ -1,59 +0,0 @@
use acp_thread::AcpThread;
use anyhow::Result;
use context_server::{
listener::{McpServerTool, ToolResponse},
types::{ToolAnnotations, ToolResponseContent},
};
use gpui::{AsyncApp, WeakEntity};
use crate::tools::ReadToolParams;
#[derive(Clone)]
pub struct ReadTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl ReadTool {
pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { thread_rx }
}
}
impl McpServerTool for ReadTool {
type Input = ReadToolParams;
type Output = ();
const NAME: &'static str = "Read";
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Read file".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: None,
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![ToolResponseContent::Text { text: content }],
structured_content: (),
})
}
}

View File

@@ -1,688 +0,0 @@
use std::path::PathBuf;
use agent_client_protocol as acp;
use itertools::Itertools;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use util::ResultExt;
pub enum ClaudeTool {
Task(Option<TaskToolParams>),
NotebookRead(Option<NotebookReadToolParams>),
NotebookEdit(Option<NotebookEditToolParams>),
Edit(Option<EditToolParams>),
MultiEdit(Option<MultiEditToolParams>),
ReadFile(Option<ReadToolParams>),
Write(Option<WriteToolParams>),
Ls(Option<LsToolParams>),
Glob(Option<GlobToolParams>),
Grep(Option<GrepToolParams>),
Terminal(Option<BashToolParams>),
WebFetch(Option<WebFetchToolParams>),
WebSearch(Option<WebSearchToolParams>),
TodoWrite(Option<TodoWriteToolParams>),
ExitPlanMode(Option<ExitPlanModeToolParams>),
Other {
name: String,
input: serde_json::Value,
},
}
impl ClaudeTool {
pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
match tool_name {
// Known tools
"mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
"mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
"mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()),
"MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
"Write" => Self::Write(serde_json::from_value(input).log_err()),
"LS" => Self::Ls(serde_json::from_value(input).log_err()),
"Glob" => Self::Glob(serde_json::from_value(input).log_err()),
"Grep" => Self::Grep(serde_json::from_value(input).log_err()),
"Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
"WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
"WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
"TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
"exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
"Task" => Self::Task(serde_json::from_value(input).log_err()),
"NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
"NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
// Inferred from name
_ => {
let tool_name = tool_name.to_lowercase();
if tool_name.contains("edit") || tool_name.contains("write") {
Self::Edit(None)
} else if tool_name.contains("terminal") {
Self::Terminal(None)
} else {
Self::Other {
name: tool_name,
input,
}
}
}
}
}
pub fn label(&self) -> String {
match &self {
Self::Task(Some(params)) => params.description.clone(),
Self::Task(None) => "Task".into(),
Self::NotebookRead(Some(params)) => {
format!("Read Notebook {}", params.notebook_path.display())
}
Self::NotebookRead(None) => "Read Notebook".into(),
Self::NotebookEdit(Some(params)) => {
format!("Edit Notebook {}", params.notebook_path.display())
}
Self::NotebookEdit(None) => "Edit Notebook".into(),
Self::Terminal(Some(params)) => format!("`{}`", params.command),
Self::Terminal(None) => "Terminal".into(),
Self::ReadFile(_) => "Read File".into(),
Self::Ls(Some(params)) => {
format!("List Directory {}", params.path.display())
}
Self::Ls(None) => "List Directory".into(),
Self::Edit(Some(params)) => {
format!("Edit {}", params.abs_path.display())
}
Self::Edit(None) => "Edit".into(),
Self::MultiEdit(Some(params)) => {
format!("Multi Edit {}", params.file_path.display())
}
Self::MultiEdit(None) => "Multi Edit".into(),
Self::Write(Some(params)) => {
format!("Write {}", params.abs_path.display())
}
Self::Write(None) => "Write".into(),
Self::Glob(Some(params)) => {
format!("Glob `{params}`")
}
Self::Glob(None) => "Glob".into(),
Self::Grep(Some(params)) => format!("`{params}`"),
Self::Grep(None) => "Grep".into(),
Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
Self::WebFetch(None) => "Fetch".into(),
Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
Self::WebSearch(None) => "Web Search".into(),
Self::TodoWrite(Some(params)) => format!(
"Update TODOs: {}",
params.todos.iter().map(|todo| &todo.content).join(", ")
),
Self::TodoWrite(None) => "Update TODOs".into(),
Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
Self::Other { name, .. } => name.clone(),
}
}
pub fn content(&self) -> Vec<acp::ToolCallContent> {
match &self {
Self::Other { input, .. } => vec![
format!(
"```json\n{}```",
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
)
.into(),
],
Self::Task(Some(params)) => vec![params.prompt.clone().into()],
Self::NotebookRead(Some(params)) => {
vec![params.notebook_path.display().to_string().into()]
}
Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
Self::Terminal(Some(params)) => vec![
format!(
"`{}`\n\n{}",
params.command,
params.description.as_deref().unwrap_or_default()
)
.into(),
],
Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
Self::Glob(Some(params)) => vec![params.to_string().into()],
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: Some(params.old_text.clone()),
new_text: params.new_text.clone(),
},
}],
Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: None,
new_text: params.content.clone(),
},
}],
Self::MultiEdit(Some(params)) => {
// todo: show multiple edits in a multibuffer?
params
.edits
.first()
.map(|edit| {
vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: Some(edit.old_string.clone()),
new_text: edit.new_string.clone(),
},
}]
})
.unwrap_or_default()
}
Self::TodoWrite(Some(_)) => {
// These are mapped to plan updates later
vec![]
}
Self::Task(None)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
| Self::Terminal(None)
| Self::ReadFile(None)
| Self::Ls(None)
| Self::Glob(None)
| Self::Grep(None)
| Self::WebFetch(None)
| Self::WebSearch(None)
| Self::TodoWrite(None)
| Self::ExitPlanMode(None)
| Self::Edit(None)
| Self::Write(None)
| Self::MultiEdit(None) => vec![],
}
}
pub fn kind(&self) -> acp::ToolKind {
match self {
Self::Task(_) => acp::ToolKind::Think,
Self::NotebookRead(_) => acp::ToolKind::Read,
Self::NotebookEdit(_) => acp::ToolKind::Edit,
Self::Edit(_) => acp::ToolKind::Edit,
Self::MultiEdit(_) => acp::ToolKind::Edit,
Self::Write(_) => acp::ToolKind::Edit,
Self::ReadFile(_) => acp::ToolKind::Read,
Self::Ls(_) => acp::ToolKind::Search,
Self::Glob(_) => acp::ToolKind::Search,
Self::Grep(_) => acp::ToolKind::Search,
Self::Terminal(_) => acp::ToolKind::Execute,
Self::WebSearch(_) => acp::ToolKind::Search,
Self::WebFetch(_) => acp::ToolKind::Fetch,
Self::TodoWrite(_) => acp::ToolKind::Think,
Self::ExitPlanMode(_) => acp::ToolKind::Think,
Self::Other { .. } => acp::ToolKind::Other,
}
}
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
match &self {
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: None,
}],
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::Write(Some(WriteToolParams {
abs_path: file_path,
..
})) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::ReadFile(Some(ReadToolParams {
abs_path, offset, ..
})) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: *offset,
}],
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::Glob(Some(GlobToolParams {
path: Some(path), ..
})) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Grep(Some(GrepToolParams {
path: Some(path), ..
})) => vec![acp::ToolCallLocation {
path: PathBuf::from(path),
line: None,
}],
Self::Task(_)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
| Self::Edit(None)
| Self::MultiEdit(None)
| Self::Write(None)
| Self::ReadFile(None)
| Self::Ls(None)
| Self::Glob(_)
| Self::Grep(_)
| Self::Terminal(_)
| Self::WebFetch(_)
| Self::WebSearch(_)
| Self::TodoWrite(_)
| Self::ExitPlanMode(_)
| Self::Other { .. } => vec![],
}
}
pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
acp::ToolCall {
id,
kind: self.kind(),
status: acp::ToolCallStatus::InProgress,
title: self.label(),
content: self.content(),
locations: self.locations(),
raw_input: None,
raw_output: None,
}
}
}
/// Edit a file.
///
/// In sessions with mcp__zed__Edit always use it instead of Edit as it will
/// allow the user to conveniently review changes.
///
/// File editing instructions:
/// - The `old_text` param must match existing file content, including indentation.
/// - The `old_text` param must come from the actual file, not an outline.
/// - The `old_text` section must not be empty.
/// - Be minimal with replacements:
/// - For unique lines, include only those lines.
/// - For non-unique lines, include enough context to identify them.
/// - Do not escape quotes, newlines, or other characters.
/// - Only edit the specified file.
#[derive(Deserialize, JsonSchema, Debug)]
pub struct EditToolParams {
/// The absolute path to the file to read.
pub abs_path: PathBuf,
/// The old text to replace (must be unique in the file)
pub old_text: String,
/// The new text.
pub new_text: String,
}
/// Reads the content of the given file in the project.
///
/// Never attempt to read a path that hasn't been previously mentioned.
///
/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents.
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ReadToolParams {
/// The absolute path to the file to read.
pub abs_path: PathBuf,
/// Which line to start reading from. Omit to start from the beginning.
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
/// How many lines to read. Omit for the whole file.
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
/// Writes content to the specified file in the project.
///
/// In sessions with mcp__zed__Write always use it instead of Write as it will
/// allow the user to conveniently review changes.
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WriteToolParams {
/// The absolute path of the file to write.
pub abs_path: PathBuf,
/// The full content to write.
pub content: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct BashToolParams {
/// Shell command to execute
pub command: String,
/// 5-10 word description of what command does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Timeout in ms (max 600000ms/10min, default 120000ms)
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct GlobToolParams {
/// Glob pattern like **/*.js or src/**/*.ts
pub pattern: String,
/// Directory to search in (omit for current directory)
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
}
impl std::fmt::Display for GlobToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(path) = &self.path {
write!(f, "{}", path.display())?;
}
write!(f, "{}", self.pattern)
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct LsToolParams {
/// Absolute path to directory
pub path: PathBuf,
/// Array of glob patterns to ignore
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ignore: Vec<String>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct GrepToolParams {
/// Regex pattern to search for
pub pattern: String,
/// File/directory to search (defaults to current directory)
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// "content" (shows lines), "files_with_matches" (default), "count"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_mode: Option<GrepOutputMode>,
/// Filter files with glob pattern like "*.js"
#[serde(skip_serializing_if = "Option::is_none")]
pub glob: Option<String>,
/// File type filter like "js", "py", "rust"
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub file_type: Option<String>,
/// Case insensitive search
#[serde(rename = "-i", default, skip_serializing_if = "is_false")]
pub case_insensitive: bool,
/// Show line numbers (content mode only)
#[serde(rename = "-n", default, skip_serializing_if = "is_false")]
pub line_numbers: bool,
/// Lines after match (content mode only)
#[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
pub after_context: Option<u32>,
/// Lines before match (content mode only)
#[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
pub before_context: Option<u32>,
/// Lines before and after match (content mode only)
#[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
pub context: Option<u32>,
/// Enable multiline/cross-line matching
#[serde(default, skip_serializing_if = "is_false")]
pub multiline: bool,
/// Limit output to first N results
#[serde(skip_serializing_if = "Option::is_none")]
pub head_limit: Option<u32>,
}
impl std::fmt::Display for GrepToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "grep")?;
// Boolean flags
if self.case_insensitive {
write!(f, " -i")?;
}
if self.line_numbers {
write!(f, " -n")?;
}
// Context options
if let Some(after) = self.after_context {
write!(f, " -A {}", after)?;
}
if let Some(before) = self.before_context {
write!(f, " -B {}", before)?;
}
if let Some(context) = self.context {
write!(f, " -C {}", context)?;
}
// Output mode
if let Some(mode) = &self.output_mode {
match mode {
GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
GrepOutputMode::Count => write!(f, " -c")?,
GrepOutputMode::Content => {} // Default mode
}
}
// Head limit
if let Some(limit) = self.head_limit {
write!(f, " | head -{}", limit)?;
}
// Glob pattern
if let Some(glob) = &self.glob {
write!(f, " --include=\"{}\"", glob)?;
}
// File type
if let Some(file_type) = &self.file_type {
write!(f, " --type={}", file_type)?;
}
// Multiline
if self.multiline {
write!(f, " -P")?; // Perl-compatible regex for multiline
}
// Pattern (escaped if contains special characters)
write!(f, " \"{}\"", self.pattern)?;
// Path
if let Some(path) = &self.path {
write!(f, " {}", path)?;
}
Ok(())
}
}
#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoPriority {
High,
#[default]
Medium,
Low,
}
impl Into<acp::PlanEntryPriority> for TodoPriority {
fn into(self) -> acp::PlanEntryPriority {
match self {
TodoPriority::High => acp::PlanEntryPriority::High,
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
TodoPriority::Low => acp::PlanEntryPriority::Low,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoStatus {
Pending,
InProgress,
Completed,
}
impl Into<acp::PlanEntryStatus> for TodoStatus {
fn into(self) -> acp::PlanEntryStatus {
match self {
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct Todo {
/// Task description
pub content: String,
/// Current status of the todo
pub status: TodoStatus,
/// Priority level of the todo
#[serde(default)]
pub priority: TodoPriority,
}
impl Into<acp::PlanEntry> for Todo {
fn into(self) -> acp::PlanEntry {
acp::PlanEntry {
content: self.content,
priority: self.priority.into(),
status: self.status.into(),
}
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TodoWriteToolParams {
pub todos: Vec<Todo>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ExitPlanModeToolParams {
/// Implementation plan in markdown format
pub plan: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TaskToolParams {
/// Short 3-5 word description of task
pub description: String,
/// Detailed task for agent to perform
pub prompt: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct NotebookReadToolParams {
/// Absolute path to .ipynb file
pub notebook_path: PathBuf,
/// Specific cell ID to read
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_id: Option<String>,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CellType {
Code,
Markdown,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EditMode {
Replace,
Insert,
Delete,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct NotebookEditToolParams {
/// Absolute path to .ipynb file
pub notebook_path: PathBuf,
/// New cell content
pub new_source: String,
/// Cell ID to edit
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_id: Option<String>,
/// Type of cell (code or markdown)
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_type: Option<CellType>,
/// Edit operation mode
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_mode: Option<EditMode>,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct MultiEditItem {
/// The text to search for and replace
pub old_string: String,
/// The replacement text
pub new_string: String,
/// Whether to replace all occurrences or just the first
#[serde(default, skip_serializing_if = "is_false")]
pub replace_all: bool,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct MultiEditToolParams {
/// Absolute path to file
pub file_path: PathBuf,
/// List of edits to apply
pub edits: Vec<MultiEditItem>,
}
fn is_false(v: &bool) -> bool {
!*v
}
#[derive(Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GrepOutputMode {
Content,
FilesWithMatches,
Count,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WebFetchToolParams {
/// Valid URL to fetch
#[serde(rename = "url")]
pub url: String,
/// What to extract from content
pub prompt: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WebSearchToolParams {
/// Search query (min 2 chars)
pub query: String,
/// Only include these domains
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_domains: Vec<String>,
/// Exclude these domains
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_domains: Vec<String>,
}
impl std::fmt::Display for WebSearchToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.query)?;
if !self.allowed_domains.is_empty() {
write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
}
if !self.blocked_domains.is_empty() {
write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
}
Ok(())
}
}

View File

@@ -1,59 +0,0 @@
use acp_thread::AcpThread;
use anyhow::Result;
use context_server::{
listener::{McpServerTool, ToolResponse},
types::ToolAnnotations,
};
use gpui::{AsyncApp, WeakEntity};
use crate::tools::WriteToolParams;
#[derive(Clone)]
pub struct WriteTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl WriteTool {
pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { thread_rx }
}
}
impl McpServerTool for WriteTool {
type Input = WriteToolParams;
type Output = ();
const NAME: &'static str = "Write";
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Write file".to_string()),
read_only_hint: Some(false),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: Some(false),
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
thread
.update(cx, |thread, cx| {
thread.write_text_file(input.abs_path, input.content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![],
structured_content: (),
})
}
}

View File

@@ -1,8 +1,7 @@
use crate::{AgentServerCommand, AgentServerSettings};
use crate::{AgentServerCommand, AgentServerDelegate};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use gpui::{App, SharedString, Task};
use std::{path::Path, rc::Rc};
use ui::IconName;
@@ -13,11 +12,8 @@ pub struct CustomAgentServer {
}
impl CustomAgentServer {
pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
Self {
name,
command: settings.command.clone(),
}
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
Self { name, command }
}
}
@@ -34,27 +30,16 @@ impl crate::AgentServer for CustomAgentServer {
IconName::Terminal
}
fn empty_state_headline(&self) -> SharedString {
"No conversations yet".into()
}
fn empty_state_message(&self) -> SharedString {
format!("Start a conversation with {}", self.name).into()
}
fn connect(
&self,
root_dir: &Path,
_project: &Entity<Project>,
_delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |mut cx| {
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
})
cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await)
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {

View File

@@ -1,4 +1,6 @@
use crate::AgentServer;
use crate::{AgentServer, AgentServerDelegate};
#[cfg(test)]
use crate::{AgentServerCommand, CustomAgentServerSettings};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
@@ -471,12 +473,14 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
claude: Some(crate::AgentServerSettings {
command: crate::claude::tests::local_command(),
}),
gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(),
claude: Some(CustomAgentServerSettings {
command: AgentServerCommand {
path: "claude-code-acp".into(),
args: vec![],
env: None,
},
}),
gemini: Some(crate::gemini::tests::local_command().into()),
custom: collections::HashMap::default(),
},
cx,
@@ -494,8 +498,10 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let delegate = AgentServerDelegate::new(project.clone(), None);
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), &project, cx))
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
.await
.unwrap();

View File

@@ -1,12 +1,12 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerCommand};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::Project;
use settings::SettingsStore;
use crate::AllAgentServersSettings;
@@ -25,14 +25,6 @@ impl AgentServer for Gemini {
"Gemini CLI".into()
}
fn empty_state_headline(&self) -> SharedString {
self.name()
}
fn empty_state_message(&self) -> SharedString {
"Ask questions, edit files, run commands".into()
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiGemini
}
@@ -40,60 +32,108 @@ impl AgentServer for Gemini {
fn connect(
&self,
root_dir: &Path,
project: &Entity<Project>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let project = project.clone();
let root_dir = root_dir.to_path_buf();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
});
cx.spawn(async move |cx| {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
})?;
let Some(mut command) =
AgentServerCommand::resolve("gemini", &[ACP_ARG], None, settings, &project, cx).await
else {
return Err(LoadError::NotInstalled {
error_message: "Failed to find Gemini CLI binary".into(),
install_message: "Install Gemini CLI".into(),
install_command: Self::install_command().into(),
}.into());
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
ignore_system_version,
Some(Self::MINIMUM_VERSION.parse().unwrap()),
cx,
)
})?
.await?
};
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
if let Some(api_key)= cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command.env.get_or_insert_default().insert("GEMINI_API_KEY".to_owned(), api_key.key);
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
if result.is_err() {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
match &result {
Ok(connection) => {
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
&& !connection.prompt_capabilities().image
{
let version_output = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output()
.await;
let current_version =
String::from_utf8(version_output?.stdout)?.trim().to_owned();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
Err(e) => {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let (version_output, help_output) = futures::future::join(version_fut, help_fut).await;
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let current_version = String::from_utf8(version_output?.stdout)?;
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
let (version_output, help_output) =
futures::future::join(version_fut, help_fut).await;
let Some(version_output) = version_output.ok().and_then(|output| String::from_utf8(output.stdout).ok()) else {
return result;
};
let Some((help_stdout, help_stderr)) = help_output.ok().and_then(|output| String::from_utf8(output.stdout).ok().zip(String::from_utf8(output.stderr).ok())) else {
return result;
};
if !supported {
return Err(LoadError::Unsupported {
error_message: format!(
"Your installed version of Gemini CLI ({}, version {}) doesn't support the Agentic Coding Protocol (ACP).",
command.path.to_string_lossy(),
current_version
).into(),
upgrade_message: "Upgrade Gemini CLI to latest".into(),
upgrade_command: Self::upgrade_command().into(),
}.into())
let current_version = version_output.trim().to_string();
let supported = help_stdout.contains(ACP_ARG) || current_version.parse::<semver::Version>().is_ok_and(|version| version >= Self::MINIMUM_VERSION.parse::<semver::Version>().unwrap());
log::error!("failed to create ACP connection to gemini (version is {current_version}, supported: {supported}): {e}");
log::debug!("gemini --help stdout: {help_stdout:?}");
log::debug!("gemini --help stderr: {help_stderr:?}");
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
}
result
@@ -106,17 +146,11 @@ impl AgentServer for Gemini {
}
impl Gemini {
pub fn binary_name() -> &'static str {
"gemini"
}
const PACKAGE_NAME: &str = "@google/gemini-cli";
pub fn install_command() -> &'static str {
"npm install -g @google/gemini-cli@preview"
}
const MINIMUM_VERSION: &str = "0.2.1";
pub fn upgrade_command() -> &'static str {
"npm install -g @google/gemini-cli@preview"
}
const BINARY_NAME: &str = "gemini";
}
#[cfg(test)]

View File

@@ -1,27 +1,75 @@
use std::path::PathBuf;
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>,
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<CustomAgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, AgentServerSettings>,
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct BuiltinAgentServerSettings {
/// Absolute path to a binary to be used when launching this agent.
///
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
#[serde(rename = "command")]
pub path: Option<PathBuf>,
/// If a binary is specified in `command`, it will be passed these arguments.
pub args: Option<Vec<String>>,
/// If a binary is specified in `command`, it will be passed these environment variables.
pub env: Option<HashMap<String, String>>,
/// Whether to skip searching `$PATH` for an agent server binary when
/// launching this agent.
///
/// This has no effect if a `command` is specified. Otherwise, when this is
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
/// is found, use it for threads with this agent. If no agent binary is
/// found on `$PATH`, Zed will automatically install and use its own binary.
/// When this is `true`, Zed will not search `$PATH`, and will always use
/// its own binary.
///
/// Default: true
pub ignore_system_version: Option<bool>,
}
impl BuiltinAgentServerSettings {
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
self.path.map(|path| AgentServerCommand {
path,
args: self.args.unwrap_or_default(),
env: self.env,
})
}
}
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
fn from(value: AgentServerCommand) -> Self {
BuiltinAgentServerSettings {
path: Some(value.path),
args: Some(value.args),
env: value.env,
..Default::default()
}
}
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct AgentServerSettings {
pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}

View File

@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -48,7 +48,7 @@ pub enum NotifyWhenAgentWaiting {
Never,
}
#[derive(Default, Clone, Debug)]
#[derive(Default, Clone, Debug, SettingsUi)]
pub struct AgentSettings {
pub enabled: bool,
pub button: bool,
@@ -352,18 +352,19 @@ impl JsonSchema for LanguageModelProviderSetting {
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"enum": [
"anthropic",
"amazon-bedrock",
"google",
"lmstudio",
"ollama",
"openai",
"zed.dev",
"anthropic",
"copilot_chat",
"deepseek",
"openrouter",
"google",
"lmstudio",
"mistral",
"vercel"
"ollama",
"openai",
"openrouter",
"vercel",
"x_ai",
"zed.dev"
]
})
}

View File

@@ -80,6 +80,7 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true

View File

@@ -6,8 +6,8 @@ use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
TextStyleRefinement, WeakEntity, Window,
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use project::Project;
@@ -154,10 +154,22 @@ impl EntryViewState {
});
}
}
AgentThreadEntry::AssistantMessage(_) => {
if index == self.entries.len() {
self.entries.push(Entry::empty())
}
AgentThreadEntry::AssistantMessage(message) => {
let entry = if let Some(Entry::AssistantMessage(entry)) =
self.entries.get_mut(index)
{
entry
} else {
self.set_entry(
index,
Entry::AssistantMessage(AssistantMessageEntry::default()),
);
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
unreachable!()
};
entry
};
entry.sync(message);
}
};
}
@@ -177,7 +189,7 @@ impl EntryViewState {
pub fn settings_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } => {}
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
Entry::Content(response_views) => {
for view in response_views.values() {
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
@@ -208,17 +220,44 @@ pub enum ViewEvent {
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
}
#[derive(Default, Debug)]
pub struct AssistantMessageEntry {
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
}
impl AssistantMessageEntry {
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
self.scroll_handles_by_chunk_index.get(&ix).cloned()
}
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
let ix = message.chunks.len() - 1;
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
handle.scroll_to_bottom();
}
}
}
#[derive(Debug)]
pub enum Entry {
UserMessage(Entity<MessageEditor>),
AssistantMessage(AssistantMessageEntry),
Content(HashMap<EntityId, AnyEntity>),
}
impl Entry {
pub fn focus_handle(&self, cx: &App) -> Option<FocusHandle> {
match self {
Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)),
Self::AssistantMessage(_) | Self::Content(_) => None,
}
}
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
match self {
Self::UserMessage(editor) => Some(editor),
Entry::Content(_) => None,
Self::AssistantMessage(_) | Self::Content(_) => None,
}
}
@@ -239,6 +278,16 @@ impl Entry {
.map(|entity| entity.downcast::<TerminalView>().unwrap())
}
pub fn scroll_handle_for_assistant_message_chunk(
&self,
chunk_ix: usize,
) -> Option<ScrollHandle> {
match self {
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
Self::UserMessage(_) | Self::Content(_) => None,
}
}
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
match self {
Self::Content(map) => Some(map),
@@ -254,7 +303,7 @@ impl Entry {
pub fn has_content(&self) -> bool {
match self {
Self::Content(map) => !map.is_empty(),
Self::UserMessage(_) => false,
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
}
}
}

View File

@@ -4,7 +4,7 @@ use crate::{
};
use acp_thread::{MentionUri, selection_name};
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent_servers::{AgentServer, AgentServerDelegate};
use agent2::HistoryStore;
use anyhow::{Result, anyhow};
use assistant_slash_commands::codeblock_fence_for_path;
@@ -438,13 +438,16 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
return Task::ready(Err(anyhow!("project entry not found")));
};
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
let directory_path = entry.path.clone();
let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("worktree not found")));
};
let project = self.project.clone();
cx.spawn(async move |_, cx| {
let directory_path = entry.path.clone();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
let file_paths = worktree.read_with(cx, |worktree, _cx| {
collect_files_in_path(worktree, &directory_path)
@@ -645,7 +648,8 @@ impl MessageEditor {
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
let connection = server.connect(Path::new(""), &self.project, cx);
let delegate = AgentServerDelegate::new(self.project.clone(), None);
let connection = server.connect(Path::new(""), delegate, cx);
cx.spawn(async move |_, cx| {
let agent = connection.await?;
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
@@ -2127,7 +2131,7 @@ mod tests {
lsp::SymbolInformation {
name: "MySymbol".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 1),

View File

@@ -73,11 +73,8 @@ impl AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
this.delegate.update_matches(this.query(cx), window, cx)
})?
.await;
Ok(())
this.refresh(window, cx)
})
}
refresh(&this, &session_id, cx).await.log_err();

File diff suppressed because it is too large Load Diff

View File

@@ -3,20 +3,23 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
use std::{sync::Arc, time::Duration};
use std::{ops::Range, sync::Arc, time::Duration};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
WeakEntity, percentage,
};
use language::LanguageRegistry;
use language_model::{
@@ -24,7 +27,6 @@ use language_model::{
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
Project,
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
@@ -34,7 +36,7 @@ use ui::{
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::Workspace;
use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
@@ -49,7 +51,6 @@ pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>,
@@ -59,7 +60,6 @@ pub struct AgentConfiguration {
_registry_subscription: Subscription,
scroll_handle: ScrollHandle,
scrollbar_state: ScrollbarState,
gemini_is_installed: bool,
_check_for_gemini: Task<()>,
}
@@ -70,7 +70,6 @@ impl AgentConfiguration {
tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -95,11 +94,6 @@ impl AgentConfiguration {
cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
.detach();
cx.observe_global_in::<SettingsStore>(window, |this, _, cx| {
this.check_for_gemini(cx);
cx.notify();
})
.detach();
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
@@ -108,7 +102,6 @@ impl AgentConfiguration {
fs,
language_registry,
workspace,
project,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
@@ -118,11 +111,9 @@ impl AgentConfiguration {
_registry_subscription: registry_subscription,
scroll_handle,
scrollbar_state,
gemini_is_installed: false,
_check_for_gemini: Task::ready(()),
};
this.build_provider_configuration_views(window, cx);
this.check_for_gemini(cx);
this
}
@@ -152,34 +143,6 @@ impl AgentConfiguration {
self.configuration_views_by_provider
.insert(provider.id(), configuration_view);
}
fn check_for_gemini(&mut self, cx: &mut Context<Self>) {
let project = self.project.clone();
let settings = AllAgentServersSettings::get_global(cx).clone();
self._check_for_gemini = cx.spawn({
async move |this, cx| {
let Some(project) = project.upgrade() else {
return;
};
let gemini_is_installed = AgentServerCommand::resolve(
Gemini::binary_name(),
&[],
// TODO expose fallback path from the Gemini/CC types so we don't have to hardcode it again here
None,
settings.gemini,
&project,
cx,
)
.await
.is_some();
this.update(cx, |this, cx| {
this.gemini_is_installed = gemini_is_installed;
cx.notify();
})
.ok();
}
});
}
}
impl Focusable for AgentConfiguration {
@@ -368,6 +331,7 @@ impl AgentConfiguration {
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
@@ -1038,9 +1002,8 @@ impl AgentConfiguration {
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
settings: settings.clone(),
command: settings.command.clone(),
},
None,
cx,
)
.into_any_element()
@@ -1058,10 +1021,40 @@ impl AgentConfiguration {
.child(
v_flex()
.gap_0p5()
.child(Headline::new("External Agents"))
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
.child(Headline::new("External Agents"))
.child(
Button::new("add-agent", "Add Agent")
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(
move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace,
cx,
).await
})
.detach_and_log_err(cx);
}
}
),
)
)
.child(
Label::new(
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
"All agents connected through the Agent Client Protocol.",
)
.color(Color::Muted),
),
@@ -1070,10 +1063,14 @@ impl AgentConfiguration {
IconName::AiGemini,
"Gemini CLI",
ExternalAgent::Gemini,
(!self.gemini_is_installed).then_some(Gemini::install_command().into()),
cx,
))
// TODO add CC
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
ExternalAgent::ClaudeCode,
cx,
))
.children(user_defined_agents),
)
}
@@ -1083,7 +1080,6 @@ impl AgentConfiguration {
icon: IconName,
name: impl Into<SharedString>,
agent: ExternalAgent,
install_command: Option<SharedString>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
@@ -1103,88 +1099,26 @@ impl AgentConfiguration {
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
.child(Label::new(name.clone())),
)
.map(|this| {
if let Some(install_command) = install_command {
this.child(
Button::new(
SharedString::from(format!("install_external_agent-{name}")),
"Install Agent",
)
.label_size(LabelSize::Small)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(Tooltip::text(install_command.clone()))
.on_click(cx.listener(
move |this, _, window, cx| {
let Some(project) = this.project.upgrade() else {
return;
};
let Some(workspace) = this.workspace.upgrade() else {
return;
};
let cwd = project.read(cx).first_project_directory(cx);
let shell =
project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let spawn_in_terminal = task::SpawnInTerminal {
id: task::TaskId(install_command.to_string()),
full_label: install_command.to_string(),
label: install_command.to_string(),
command: Some(install_command.to_string()),
args: Vec::new(),
command_label: install_command.to_string(),
cwd,
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: true,
reveal: Default::default(),
reveal_target: Default::default(),
hide: Default::default(),
shell,
show_summary: true,
show_command: true,
show_rerun: false,
};
let task = workspace.update(cx, |workspace, cx| {
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
});
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.check_for_gemini(cx);
})
.ok();
})
.detach();
},
)),
)
} else {
this.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
)
}
})
.child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
)
}
}
@@ -1324,3 +1258,109 @@ fn show_unable_to_uninstall_extension_with_context_server(
workspace.toggle_status_toast(status_toast, cx);
}
async fn open_new_agent_servers_entry_in_settings_editor(
workspace: WeakEntity<Workspace>,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let settings_editor = workspace
.update_in(cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?
.await?
.downcast::<Editor>()
.unwrap();
settings_editor
.downgrade()
.update_in(cx, |item, window, cx| {
let text = item.buffer().read(cx).snapshot(cx).text();
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
if i == 0 {
"your_agent".into()
} else {
format!("your_agent_{}", i).into()
}
})
.find(|name| !file.custom.contains_key(name));
if let Some(server_name) = server_name {
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
CustomAgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
},
);
}
});
if edits.is_empty() {
return;
}
let ranges = edits
.iter()
.map(|(range, _)| range.clone())
.collect::<Vec<_>>();
item.edit(edits, cx);
if let Some((unique_server_name, buffer)) =
unique_server_name.zip(item.buffer().read(cx).as_singleton())
{
let snapshot = buffer.read(cx).snapshot();
if let Some(range) =
find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot)
{
item.change_selections(
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
}
}
})
}
fn find_text_in_buffer(
text: &str,
start: usize,
snapshot: &language::BufferSnapshot,
) -> Option<Range<usize>> {
let chars = text.chars().collect::<Vec<char>>();
let mut offset = start;
let mut char_offset = 0;
for c in snapshot.chars_at(start) {
if char_offset >= chars.len() {
break;
}
offset += 1;
if c == chars[char_offset] {
char_offset += 1;
} else {
char_offset = 0;
}
}
if char_offset == chars.len() {
Some(offset.saturating_sub(chars.len())..offset)
} else {
None
}
}

View File

@@ -10,6 +10,7 @@ use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
};
use gpui::{
@@ -257,7 +258,7 @@ impl AgentDiffPane {
path_key.clone(),
buffer.clone(),
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(diff_handle, cx);

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Duration;
use acp_thread::AcpThread;
use agent_servers::AgentServerSettings;
use agent_servers::AgentServerCommand;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use serde::{Deserialize, Serialize};
@@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
use crate::ui::AcpOnboardingModal;
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -28,7 +29,6 @@ use crate::{
slash_command::SlashCommandCompletionProvider,
text_thread_editor::{
AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
render_remaining_tokens,
},
thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell},
@@ -77,13 +77,16 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
agent::{
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
ToggleModelSelector,
},
assistant::{OpenRulesLibrary, ToggleFocus},
};
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
struct SerializedAgentPanel {
width: Option<Pixels>,
selected_agent: Option<AgentType>,
@@ -201,6 +204,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
AgentOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
AcpOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -253,7 +259,7 @@ pub enum AgentType {
NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
command: AgentServerCommand,
},
}
@@ -278,6 +284,17 @@ impl AgentType {
}
}
impl From<ExternalAgent> for AgentType {
fn from(value: ExternalAgent) -> Self {
match value {
ExternalAgent::Gemini => Self::Gemini,
ExternalAgent::ClaudeCode => Self::ClaudeCode,
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
ExternalAgent::NativeAgent => Self::NativeAgent,
}
}
}
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
@@ -586,22 +603,11 @@ impl AgentPanel {
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
serde_json::from_str::<SerializedAgentPanel>(&panel).log_err()
} else {
None
};
// Wait for the Gemini/Native feature flag to be available.
let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?;
if !client.status().borrow().is_signed_out() {
cx.update(|_, cx| {
cx.wait_for_flag_or_timeout::<feature_flags::GeminiAndNativeFeatureFlag>(
Duration::from_secs(2),
)
})?
.await;
}
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = cx.new(|cx| {
Self::new(
@@ -622,6 +628,10 @@ impl AgentPanel {
}
cx.notify();
});
} else {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(AgentType::NativeAgent, window, cx);
});
}
panel
})?;
@@ -1050,6 +1060,11 @@ impl AgentPanel {
editor
});
if self.selected_agent != AgentType::TextThread {
self.selected_agent = AgentType::TextThread;
self.serialize(cx);
}
self.set_active_view(
ActiveView::prompt_editor(
context_editor.clone(),
@@ -1141,6 +1156,12 @@ impl AgentPanel {
}
}
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
this.selected_agent = selected_agent;
this.serialize(cx);
}
let thread_view = cx.new(|cx| {
crate::acp::AcpThreadView::new(
server,
@@ -1236,6 +1257,12 @@ impl AgentPanel {
cx,
)
});
if self.selected_agent != AgentType::TextThread {
self.selected_agent = AgentType::TextThread;
self.serialize(cx);
}
self.set_active_view(
ActiveView::prompt_editor(
editor,
@@ -1480,7 +1507,6 @@ impl AgentPanel {
tools,
self.language_registry.clone(),
self.workspace.clone(),
self.project.downgrade(),
window,
cx,
)
@@ -1852,19 +1878,6 @@ impl AgentPanel {
menu
}
pub fn set_selected_agent(
&mut self,
agent: AgentType,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
self.new_agent_thread(agent, window, cx);
}
pub fn selected_agent(&self) -> AgentType {
self.selected_agent.clone()
}
@@ -1905,8 +1918,8 @@ impl AgentPanel {
window,
cx,
),
AgentType::Custom { name, settings } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, settings }),
AgentType::Custom { name, command } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, command }),
None,
None,
window,
@@ -2124,7 +2137,7 @@ impl AgentPanel {
.child(title_editor)
.into_any_element()
} else {
Label::new(thread_view.read(cx).title())
Label::new(thread_view.read(cx).title(cx))
.color(Color::Muted)
.truncate()
.into_any_element()
@@ -2555,7 +2568,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
panel.new_agent_thread(
AgentType::NativeAgent,
window,
cx,
@@ -2581,7 +2594,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
panel.new_agent_thread(
AgentType::TextThread,
window,
cx,
@@ -2609,7 +2622,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
panel.new_agent_thread(
AgentType::Gemini,
window,
cx,
@@ -2636,7 +2649,7 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
panel.new_agent_thread(
AgentType::ClaudeCode,
window,
cx,
@@ -2669,13 +2682,13 @@ impl AgentPanel {
workspace.panel::<AgentPanel>(cx)
{
panel.update(cx, |panel, cx| {
panel.set_selected_agent(
panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone(),
settings:
agent_settings
.clone(),
command: agent_settings
.command
.clone(),
},
window,
cx,
@@ -2693,9 +2706,9 @@ impl AgentPanel {
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
menu.separator().link(
"Add Your Own Agent",
"Add Other Agents",
OpenBrowser {
url: "https://agentclientprotocol.com/".into(),
url: zed_urls::external_agents_docs(cx),
}
.boxed_clone(),
)
@@ -2883,12 +2896,8 @@ impl AgentPanel {
Some(token_count)
}
ActiveView::TextThread { context_editor, .. } => {
let element = render_remaining_tokens(context_editor, cx)?;
Some(element.into_any_element())
}
ActiveView::ExternalAgentThread { .. }
| ActiveView::TextThread { .. }
| ActiveView::History
| ActiveView::Configuration => None,
}

View File

@@ -28,7 +28,7 @@ use std::rc::Rc;
use std::sync::Arc;
use agent::{Thread, ThreadId};
use agent_servers::AgentServerSettings;
use agent_servers::AgentServerCommand;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
@@ -170,7 +170,7 @@ enum ExternalAgent {
NativeAgent,
Custom {
name: SharedString,
settings: AgentServerSettings,
command: AgentServerCommand,
},
}
@@ -193,9 +193,9 @@ impl ExternalAgent {
Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
settings,
command.clone(),
)),
}
}

View File

@@ -987,7 +987,8 @@ impl MentionLink {
.read(cx)
.project()
.read(cx)
.entry_for_path(&project_path, cx)?;
.entry_for_path(&project_path, cx)?
.clone();
Some(MentionLink::File(project_path, entry))
}
Self::SYMBOL => {

View File

@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
EditorEvent::Edited { .. } => {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
workspace
.client()

View File

@@ -6,7 +6,8 @@ use feature_flags::ZedProFeatureFlag;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry,
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -76,6 +77,7 @@ pub struct LanguageModelPickerDelegate {
all_models: Arc<GroupedModels>,
filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>,
}
@@ -96,6 +98,7 @@ impl LanguageModelPickerDelegate {
selected_index: Self::get_active_model_index(&entries, get_active_model(cx)),
filtered_entries: entries,
get_active_model: Arc::new(get_active_model),
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
@@ -139,6 +142,56 @@ impl LanguageModelPickerDelegate {
.unwrap_or(0)
}
/// Authenticates all providers in the [`LanguageModelRegistry`].
///
/// We do this so that we can populate the language selector with all of the
/// models from the configured providers.
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
cx.spawn(async move |_cx| {
for (provider_id, provider_name, authenticate_task) in authenticate_all_providers {
if let Err(err) = authenticate_task.await {
if matches!(err, AuthenticateError::CredentialsNotFound) {
// Since we're authenticating these providers in the
// background for the purposes of populating the
// language selector, we don't care about providers
// where the credentials are not found.
} else {
// Some providers have noisy failure states that we
// don't want to spam the logs with every time the
// language model selector is initialized.
//
// Ideally these should have more clear failure modes
// that we know are safe to ignore here, like what we do
// with `CredentialsNotFound` above.
match provider_id.0.as_ref() {
"lmstudio" | "ollama" => {
// LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated".
//
// These fail noisily, so we don't log them.
}
"copilot_chat" => {
// Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors.
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
provider_name.0
);
}
}
}
}
}
})
}
pub fn active_model(&self, cx: &App) -> Option<ConfiguredModel> {
(self.get_active_model)(cx)
}

View File

@@ -2,10 +2,10 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
pub struct SlashCommandSettings {
/// Settings for the `/cargo-workspace` slash command.
#[serde(default)]

View File

@@ -1857,6 +1857,53 @@ impl TextThreadEditor {
.update(cx, |context, cx| context.summarize(true, cx));
}
fn render_remaining_tokens(&self, cx: &App) -> Option<impl IntoElement + use<>> {
let (token_count_color, token_count, max_token_count, tooltip) =
match token_state(&self.context, cx)? {
TokenState::NoTokensLeft {
max_token_count,
token_count,
} => (
Color::Error,
token_count,
max_token_count,
Some("Token Limit Reached"),
),
TokenState::HasMoreTokens {
max_token_count,
token_count,
over_warn_threshold,
} => {
let (color, tooltip) = if over_warn_threshold {
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
} else {
(Color::Muted, None)
};
(color, token_count, max_token_count, tooltip)
}
};
Some(
h_flex()
.id("token-count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
)
.when_some(tooltip, |element, tooltip| {
element.tooltip(Tooltip::text(tooltip))
}),
)
}
fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
@@ -2420,9 +2467,14 @@ impl Render for TextThreadEditor {
)
.child(
h_flex()
.gap_1()
.child(self.render_language_model_selector(window, cx))
.child(self.render_send_button(window, cx)),
.gap_2p5()
.children(self.render_remaining_tokens(cx))
.child(
h_flex()
.gap_1()
.child(self.render_language_model_selector(window, cx))
.child(self.render_send_button(window, cx)),
),
),
)
}
@@ -2710,58 +2762,6 @@ impl FollowableItem for TextThreadEditor {
}
}
pub fn render_remaining_tokens(
context_editor: &Entity<TextThreadEditor>,
cx: &App,
) -> Option<impl IntoElement + use<>> {
let context = &context_editor.read(cx).context;
let (token_count_color, token_count, max_token_count, tooltip) = match token_state(context, cx)?
{
TokenState::NoTokensLeft {
max_token_count,
token_count,
} => (
Color::Error,
token_count,
max_token_count,
Some("Token Limit Reached"),
),
TokenState::HasMoreTokens {
max_token_count,
token_count,
over_warn_threshold,
} => {
let (color, tooltip) = if over_warn_threshold {
(Color::Warning, Some("Token Limit is Close to Exhaustion"))
} else {
(Color::Muted, None)
};
(color, token_count, max_token_count, tooltip)
}
};
Some(
h_flex()
.id("token-count")
.gap_0p5()
.child(
Label::new(humanize_token_count(token_count))
.size(LabelSize::Small)
.color(token_count_color),
)
.child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
.child(
Label::new(humanize_token_count(max_token_count))
.size(LabelSize::Small)
.color(Color::Muted),
)
.when_some(tooltip, |element, tooltip| {
element.tooltip(Tooltip::text(tooltip))
}),
)
}
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(

View File

@@ -1,3 +1,4 @@
mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod context_pill;
@@ -6,6 +7,7 @@ mod onboarding_modal;
pub mod preview;
mod unavailable_editing_tooltip;
pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;

View File

@@ -0,0 +1,254 @@
use client::zed_urls;
use gpui::{
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
linear_color_stop, linear_gradient,
};
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
use crate::agent_panel::{AgentPanel, AgentType};
macro_rules! acp_onboarding_event {
($name:expr) => {
telemetry::event!($name, source = "ACP Onboarding");
};
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
};
}
pub struct AcpOnboardingModal {
focus_handle: FocusHandle,
workspace: Entity<Workspace>,
}
impl AcpOnboardingModal {
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
let workspace_entity = cx.entity();
workspace.toggle_modal(window, cx, |_window, cx| Self {
workspace: workspace_entity,
focus_handle: cx.focus_handle(),
});
}
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(AgentType::Gemini, window, cx);
});
}
});
cx.emit(DismissEvent);
acp_onboarding_event!("Open Panel Clicked");
}
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
cx.open_url(&zed_urls::external_agents_docs(cx));
cx.notify();
acp_onboarding_event!("Documentation Link Clicked");
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
impl Focusable for AcpOnboardingModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for AcpOnboardingModal {}
impl Render for AcpOnboardingModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let illustration_element = |label: bool, opacity: f32| {
h_flex()
.px_1()
.py_0p5()
.gap_1()
.rounded_sm()
.bg(cx.theme().colors().element_active.opacity(0.05))
.border_1()
.border_color(cx.theme().colors().border)
.border_dashed()
.child(
Icon::new(IconName::Stop)
.size(IconSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
)
.map(|this| {
if label {
this.child(
Label::new("Your Agent Here")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(
div().w_16().h_1().rounded_full().bg(cx
.theme()
.colors()
.element_active
.opacity(0.6)),
)
}
})
.opacity(opacity)
};
let illustration = h_flex()
.relative()
.h(rems_from_px(126.))
.bg(cx.theme().colors().editor_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.justify_center()
.gap_8()
.rounded_t_md()
.overflow_hidden()
.child(
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
),
)
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
0.,
linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.1),
0.9,
),
linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.),
0.,
),
)))
.child(
div()
.absolute()
.inset_0()
.size_full()
.bg(gpui::black().opacity(0.15)),
)
.child(
h_flex()
.gap_4()
.child(
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
)
.child(
Vector::new(
VectorName::AcpLogoSerif,
rems_from_px(111.),
rems_from_px(41.),
)
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
),
)
.child(
v_flex()
.gap_1p5()
.child(illustration_element(false, 0.15))
.child(illustration_element(true, 0.3))
.child(
h_flex()
.pl_1()
.pr_2()
.py_0p5()
.gap_1()
.rounded_sm()
.bg(cx.theme().colors().element_active.opacity(0.2))
.border_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::AiGemini)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
)
.child(illustration_element(true, 0.3))
.child(illustration_element(false, 0.15)),
);
let heading = v_flex()
.w_full()
.gap_1()
.child(
Label::new("Now Available")
.size(LabelSize::Small)
.color(Color::Muted),
)
.child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
.icon_size(IconSize::Indicator)
.style(ButtonStyle::Tinted(TintColor::Accent))
.full_width()
.on_click(cx.listener(Self::open_panel));
let docs_button = Button::new("add-other-agents", "Add Other Agents")
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::Indicator)
.icon_color(Color::Muted)
.full_width()
.on_click(cx.listener(Self::view_docs));
let close_button = h_flex().absolute().top_2().right_2().child(
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|_, _: &ClickEvent, _window, cx| {
acp_onboarding_event!("Canceled", trigger = "X click");
cx.emit(DismissEvent);
},
)),
);
v_flex()
.id("acp-onboarding")
.key_context("AcpOnboardingModal")
.relative()
.w(rems(34.))
.h_full()
.elevation_3(cx)
.track_focus(&self.focus_handle(cx))
.overflow_hidden()
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
acp_onboarding_event!("Canceled", trigger = "Action");
cx.emit(DismissEvent);
}))
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
this.focus_handle.focus(window);
}))
.child(illustration)
.child(
v_flex()
.p_4()
.gap_2()
.child(heading)
.child(Label::new(copy).color(Color::Muted))
.child(
v_flex()
.w_full()
.mt_2()
.gap_1()
.child(open_panel_button)
.child(docs_button),
),
)
.child(close_button)
}
}

View File

@@ -62,6 +62,8 @@ impl AgentNotification {
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
..Default::default()
}
}
}

View File

@@ -373,7 +373,7 @@ pub async fn complete(
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
@@ -526,7 +526,7 @@ pub async fn stream_completion_with_rate_limit_info(
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;

View File

@@ -492,7 +492,7 @@ mod custom_path_matcher {
pub fn new(globs: &[String]) -> Result<Self, globset::Error> {
let globs = globs
.iter()
.map(|glob| Glob::new(&SanitizedPath::from(glob).to_glob_string()))
.map(|glob| Glob::new(&SanitizedPath::new(glob).to_glob_string()))
.collect::<Result<Vec<_>, _>>()?;
let sources = globs.iter().map(|glob| glob.glob().to_owned()).collect();
let sources_with_trailing_slash = globs

View File

@@ -35,7 +35,7 @@ impl Tool for DeletePathTool {
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
true
}
fn may_perform_edits(&self) -> bool {

View File

@@ -11,7 +11,9 @@ use assistant_tool::{
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use editor::{
Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -474,7 +476,7 @@ impl Tool for EditFileTool {
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff, cx);
@@ -703,7 +705,7 @@ impl EditFileToolCard {
PathKey::for_buffer(buffer, cx),
buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);
@@ -791,7 +793,7 @@ impl EditFileToolCard {
path_key,
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);

View File

@@ -15,7 +15,7 @@ use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use project::{Project, terminals::TerminalKind};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -213,17 +213,16 @@ impl Tool for TerminalTool {
async move |cx| {
let program = program.await;
let env = env.await;
project
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(program),
args,
cwd,
env,
..Default::default()
}),
},
cx,
)
})?

View File

@@ -2,9 +2,9 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct AudioSettings {
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]

View File

@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use settings::{Settings, SettingsSources, SettingsStore, SettingsUi};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::{
@@ -113,6 +113,7 @@ impl Drop for MacOsUnmounter {
}
}
#[derive(SettingsUi)]
struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.

View File

@@ -16,7 +16,7 @@ use crate::windows_impl::WM_JOB_UPDATED;
type Job = fn(&Path) -> Result<()>;
#[cfg(not(test))]
pub(crate) const JOBS: [Job; 6] = [
pub(crate) const JOBS: &[Job] = &[
// Delete old files
|app_dir| {
let zed_executable = app_dir.join("Zed.exe");
@@ -32,6 +32,12 @@ pub(crate) const JOBS: [Job; 6] = [
std::fs::remove_file(&zed_cli)
.context(format!("Failed to remove old file {}", zed_cli.display()))
},
|app_dir| {
let zed_wsl = app_dir.join("bin\\zed");
log::info!("Removing old file: {}", zed_wsl.display());
std::fs::remove_file(&zed_wsl)
.context(format!("Failed to remove old file {}", zed_wsl.display()))
},
// Copy new files
|app_dir| {
let zed_executable_source = app_dir.join("install\\Zed.exe");
@@ -65,6 +71,22 @@ pub(crate) const JOBS: [Job; 6] = [
zed_cli_dest.display()
))
},
|app_dir| {
let zed_wsl_source = app_dir.join("install\\bin\\zed");
let zed_wsl_dest = app_dir.join("bin\\zed");
log::info!(
"Copying new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
);
std::fs::copy(&zed_wsl_source, &zed_wsl_dest)
.map(|_| ())
.context(format!(
"Failed to copy new file {} to {}",
zed_wsl_source.display(),
zed_wsl_dest.display()
))
},
// Clean up installer folder and updates folder
|app_dir| {
let updates_folder = app_dir.join("updates");
@@ -85,7 +107,7 @@ pub(crate) const JOBS: [Job; 6] = [
];
#[cfg(test)]
pub(crate) const JOBS: [Job; 2] = [
pub(crate) const JOBS: &[Job] = &[
|_| {
std::thread::sleep(Duration::from_millis(1000));
if let Ok(config) = std::env::var("ZED_AUTO_UPDATE") {

View File

@@ -3,6 +3,7 @@ mod models;
use anyhow::{Context, Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
use aws_sdk_bedrockruntime::types::InferenceConfiguration;
pub use aws_sdk_bedrockruntime::types::{
AnyToolChoice as BedrockAnyToolChoice, AutoToolChoice as BedrockAutoToolChoice,
ContentBlock as BedrockInnerContent, Tool as BedrockTool, ToolChoice as BedrockToolChoice,
@@ -17,7 +18,8 @@ pub use bedrock::types::{
ConverseOutput as BedrockResponse, ConverseStreamOutput as BedrockStreamingResponse,
ImageBlock as BedrockImageBlock, Message as BedrockMessage,
ReasoningContentBlock as BedrockThinkingBlock, ReasoningTextBlock as BedrockThinkingTextBlock,
ResponseStream as BedrockResponseStream, ToolResultBlock as BedrockToolResultBlock,
ResponseStream as BedrockResponseStream, SystemContentBlock as BedrockSystemContentBlock,
ToolResultBlock as BedrockToolResultBlock,
ToolResultContentBlock as BedrockToolResultContentBlock,
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
};
@@ -58,6 +60,20 @@ pub async fn stream_completion(
response = response.set_tool_config(request.tools);
}
let inference_config = InferenceConfiguration::builder()
.max_tokens(request.max_tokens as i32)
.set_temperature(request.temperature)
.set_top_p(request.top_p)
.build();
response = response.inference_config(inference_config);
if let Some(system) = request.system {
if !system.is_empty() {
response = response.system(BedrockSystemContentBlock::Text(system));
}
}
let output = response
.send()
.await

View File

@@ -151,12 +151,12 @@ impl Model {
pub fn id(&self) -> &str {
match self {
Model::ClaudeSonnet4 => "claude-4-sonnet",
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
Model::ClaudeOpus4 => "claude-4-opus",
Model::ClaudeOpus4_1 => "claude-4-opus-1",
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking",
Model::ClaudeSonnet4 => "claude-sonnet-4",
Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking",
Model::ClaudeOpus4 => "claude-opus-4",
Model::ClaudeOpus4_1 => "claude-opus-4-1",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
Model::Claude3Opus => "claude-3-opus",
@@ -359,14 +359,12 @@ impl Model {
pub fn max_output_tokens(&self) -> u64 {
match self {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet
| Self::Claude3_7SonnetThinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeOpus4
| Model::ClaudeOpus4Thinking
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Model::ClaudeOpus4_1Thinking => 128_000,
| Self::ClaudeOpus4_1Thinking => 32_000,
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
Self::Custom {
max_output_tokens, ..
@@ -784,10 +782,10 @@ mod tests {
);
// Test thinking models have different friendly IDs but same request IDs
assert_eq!(Model::ClaudeSonnet4.id(), "claude-4-sonnet");
assert_eq!(Model::ClaudeSonnet4.id(), "claude-sonnet-4");
assert_eq!(
Model::ClaudeSonnet4Thinking.id(),
"claude-4-sonnet-thinking"
"claude-sonnet-4-thinking"
);
assert_eq!(
Model::ClaudeSonnet4.request_id(),

View File

@@ -1161,7 +1161,7 @@ impl Room {
let request = self.client.request(proto::ShareProject {
room_id: self.id(),
worktrees: project.read(cx).worktree_metadata_protos(cx),
is_ssh_project: project.read(cx).is_via_ssh(),
is_ssh_project: project.read(cx).is_via_remote_server(),
});
cx.spawn(async move |this, cx| {

View File

@@ -2,9 +2,9 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, SettingsUi)]
pub struct CallSettings {
pub mute_on_join: bool,
pub share_on_join: bool,

View File

@@ -14,6 +14,7 @@ pub enum CliRequest {
paths: Vec<String>,
urls: Vec<String>,
diff_paths: Vec<[String; 2]>,
wsl: Option<String>,
wait: bool,
open_new_workspace: Option<bool>,
env: Option<HashMap<String, String>>,

View File

@@ -6,7 +6,6 @@
use anyhow::{Context as _, Result};
use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
use collections::HashMap;
use parking_lot::Mutex;
use std::{
env, fs, io,
@@ -85,6 +84,17 @@ struct Args {
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// The username and WSL distribution to use when opening paths. If not specified,
/// Zed will attempt to open the paths directly.
///
/// The username is optional, and if not specified, the default user for the distribution
/// will be used.
///
/// Example: `me@Ubuntu` or `Ubuntu`.
///
/// WARN: You should not fill in this field by hand.
#[arg(long, value_name = "USER@DISTRO")]
wsl: Option<String>,
/// Not supported in Zed CLI, only supported on Zed binary
/// Will attempt to give the correct command to run
#[arg(long)]
@@ -129,14 +139,41 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
Ok(canonicalized.to_string(|path| path.to_string_lossy().to_string()))
}
fn main() -> Result<()> {
#[cfg(all(not(debug_assertions), target_os = "windows"))]
unsafe {
use ::windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
let mut command = util::command::new_std_command("wsl.exe");
let _ = AttachConsole(ATTACH_PARENT_PROCESS);
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
if user.is_empty() {
anyhow::bail!("user is empty in wsl argument");
}
(Some(user), distro)
} else {
(None, wsl)
};
if let Some(user) = user {
command.arg("--user").arg(user);
}
let output = command
.arg("--distribution")
.arg(distro_name)
.arg("wslpath")
.arg("-m")
.arg(source)
.output()?;
let result = String::from_utf8_lossy(&output.stdout);
let prefix = format!("//wsl.localhost/{}", distro_name);
Ok(result
.trim()
.strip_prefix(&prefix)
.unwrap_or(&result)
.to_string())
}
fn main() -> Result<()> {
#[cfg(unix)]
util::prevent_root_execution();
@@ -223,6 +260,8 @@ fn main() -> Result<()> {
let env = {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
use collections::HashMap;
// On Linux, the desktop entry uses `cli` to spawn `zed`.
// We need to handle env vars correctly since std::env::vars() may not contain
// project-specific vars (e.g. those set by direnv).
@@ -235,8 +274,19 @@ fn main() -> Result<()> {
}
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
Some(std::env::vars().collect::<HashMap<_, _>>())
#[cfg(target_os = "windows")]
{
// On Windows, by default, a child process inherits a copy of the environment block of the parent process.
// So we don't need to pass env vars explicitly.
None
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "windows")))]
{
use collections::HashMap;
Some(std::env::vars().collect::<HashMap<_, _>>())
}
};
let exit_status = Arc::new(Mutex::new(None));
@@ -271,8 +321,10 @@ fn main() -> Result<()> {
paths.push(tmp_file.path().to_string_lossy().to_string());
let (tmp_file, _) = tmp_file.keep()?;
anonymous_fd_tmp_files.push((file, tmp_file));
} else if let Some(wsl) = &args.wsl {
urls.push(format!("file://{}", parse_path_in_wsl(path, wsl)?));
} else {
paths.push(parse_path_with_position(path)?)
paths.push(parse_path_with_position(path)?);
}
}
@@ -292,6 +344,7 @@ fn main() -> Result<()> {
paths,
urls,
diff_paths,
wsl: args.wsl,
wait: args.wait,
open_new_workspace,
env,
@@ -644,15 +697,15 @@ mod windows {
Storage::FileSystem::{
CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, WriteFile,
},
System::Threading::CreateMutexW,
System::Threading::{CREATE_NEW_PROCESS_GROUP, CreateMutexW},
},
core::HSTRING,
};
use crate::{Detect, InstalledApp};
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::{io, os::windows::process::CommandExt};
fn check_single_instance() -> bool {
let mutex = unsafe {
@@ -691,6 +744,7 @@ mod windows {
fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
if check_single_instance() {
std::process::Command::new(self.0.clone())
.creation_flags(CREATE_NEW_PROCESS_GROUP.0)
.arg(ipc_url)
.spawn()?;
} else {

View File

@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use std::{
any::TypeId,
convert::TryFrom,
@@ -101,7 +101,7 @@ pub struct ClientSettingsContent {
server_url: Option<String>,
}
#[derive(Deserialize)]
#[derive(Deserialize, SettingsUi)]
pub struct ClientSettings {
pub server_url: String,
}
@@ -127,7 +127,7 @@ pub struct ProxySettingsContent {
proxy: Option<String>,
}
#[derive(Deserialize, Default)]
#[derive(Deserialize, Default, SettingsUi)]
pub struct ProxySettings {
pub proxy: Option<String>,
}
@@ -287,6 +287,7 @@ pub enum Status {
},
ConnectionLost,
Reauthenticating,
Reauthenticated,
Reconnecting,
ReconnectionError {
next_reconnection: Instant,
@@ -298,6 +299,21 @@ impl Status {
matches!(self, Self::Connected { .. })
}
pub fn was_connected(&self) -> bool {
matches!(
self,
Self::ConnectionLost
| Self::Reauthenticating
| Self::Reauthenticated
| Self::Reconnecting
)
}
/// Returns whether the client is currently connected or was connected at some point.
pub fn is_or_was_connected(&self) -> bool {
self.is_connected() || self.was_connected()
}
pub fn is_signing_in(&self) -> bool {
matches!(
self,
@@ -504,7 +520,7 @@ impl<T: 'static> Drop for PendingEntitySubscription<T> {
}
}
#[derive(Copy, Clone, Deserialize, Debug)]
#[derive(Copy, Clone, Deserialize, Debug, SettingsUi)]
pub struct TelemetrySettings {
pub diagnostics: bool,
pub metrics: bool,
@@ -857,11 +873,13 @@ impl Client {
try_provider: bool,
cx: &AsyncApp,
) -> Result<Credentials> {
if self.status().borrow().is_signed_out() {
let is_reauthenticating = if self.status().borrow().is_signed_out() {
self.set_status(Status::Authenticating, cx);
false
} else {
self.set_status(Status::Reauthenticating, cx);
}
true
};
let mut credentials = None;
@@ -919,7 +937,14 @@ impl Client {
self.cloud_client
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
self.state.write().credentials = Some(credentials.clone());
self.set_status(Status::Authenticated, cx);
self.set_status(
if is_reauthenticating {
Status::Reauthenticated
} else {
Status::Authenticated
},
cx,
);
Ok(credentials)
}
@@ -1034,6 +1059,7 @@ impl Client {
| Status::Authenticating
| Status::AuthenticationError
| Status::Reauthenticating
| Status::Reauthenticated
| Status::ReconnectionError { .. } => false,
Status::Connected { .. } | Status::Connecting | Status::Reconnecting => {
return ConnectionResult::Result(Ok(()));
@@ -1670,21 +1696,10 @@ impl Client {
);
cx.spawn(async move |_| match future.await {
Ok(()) => {
log::debug!(
"rpc message handled. client_id:{}, sender_id:{:?}, type:{}",
client_id,
original_sender_id,
type_name
);
log::debug!("rpc message handled. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}");
}
Err(error) => {
log::error!(
"error handling message. client_id:{}, sender_id:{:?}, type:{}, error:{:?}",
client_id,
original_sender_id,
type_name,
error
);
log::error!("error handling message. client_id:{client_id}, sender_id:{original_sender_id:?}, type:{type_name}, error:{error:#}");
}
})
.detach();

View File

@@ -216,7 +216,9 @@ impl UserStore {
return Ok(());
};
match status {
Status::Authenticated | Status::Connected { .. } => {
Status::Authenticated
| Status::Reauthenticated
| Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let response = client
.cloud_client()

View File

@@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
server_url = server_url(cx)
)
}
/// Returns the URL to Zed AI's external agents documentation.
pub fn external_agents_docs(cx: &App) -> String {
format!(
"{server_url}/docs/ai/external-agents",
server_url = server_url(cx)
)
}

View File

@@ -138,22 +138,42 @@ pub enum LanguageModelProvider {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictEditsBody {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub outline: Option<String>,
pub input_events: String,
pub input_excerpt: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub speculated_output: Option<String>,
/// Whether the user provided consent for sampling this interaction.
#[serde(default, alias = "data_collection_permission")]
pub can_collect_data: bool,
/// Note that this is no longer sent, in favor of `PredictEditsAdditionalContext`.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub diagnostic_groups: Option<Vec<(String, serde_json::Value)>>,
/// Info about the git repository state, only present when can_collect_data is true.
/// Info about the git repository state, only present when can_collect_data is true. Note that
/// this is no longer sent, in favor of `PredictEditsAdditionalContext`.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub git_info: Option<PredictEditsGitInfo>,
}
/// Additional context only stored when can_collect_data is true for the corresponding edit
/// predictions request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictEditsAdditionalContext {
/// Path to the file in the repository that contains the input excerpt.
pub input_path: String,
/// Cursor position within the file that contains the input excerpt.
pub cursor_point: Point,
/// Cursor offset in bytes within the file that contains the input excerpt.
pub cursor_offset: usize,
#[serde(flatten)]
pub git_info: PredictEditsGitInfo,
/// Diagnostic near the cursor position.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub diagnostic_groups: Vec<(String, Box<serde_json::value::RawValue>)>,
/// True if the diagnostics were truncated.
pub diagnostic_groups_truncated: bool,
/// Recently active files that may be within this repository.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub recent_files: Vec<PredictEditsRecentFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictEditsGitInfo {
/// SHA of git HEAD commit at time of prediction.
@@ -167,6 +187,32 @@ pub struct PredictEditsGitInfo {
pub remote_upstream_url: Option<String>,
}
/// A zero-indexed point in a text buffer consisting of a row and column.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Point {
pub row: u32,
pub column: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictEditsRecentFile {
/// Path to a file within the repository.
pub path: String,
/// Most recent cursor position with the file.
pub cursor_point: Point,
/// Milliseconds between the editor for this file being active and the request time.
pub active_to_now_ms: u32,
/// Number of times the editor for this file was activated.
pub activation_count: u32,
/// Rough estimate of milliseconds the user was editing the file.
pub cumulative_time_editing_ms: u32,
/// Rough estimate of milliseconds the user was navigating within the file.
pub cumulative_time_navigating_ms: u32,
/// Whether the file is a multibuffer.
#[serde(skip_serializing_if = "is_default", default)]
pub is_multibuffer: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PredictEditsResponse {
pub request_id: Uuid,
@@ -336,6 +382,10 @@ pub struct UsageData {
pub limit: UsageLimit,
}
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;

View File

@@ -175,6 +175,7 @@ CREATE TABLE "language_servers" (
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
"name" VARCHAR NOT NULL,
"capabilities" TEXT NOT NULL,
"worktree_id" BIGINT,
PRIMARY KEY (project_id, id)
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE language_servers
ADD COLUMN worktree_id BIGINT;

View File

@@ -694,6 +694,7 @@ impl Database {
project_id: ActiveValue::set(project_id),
id: ActiveValue::set(server.id as i64),
name: ActiveValue::set(server.name.clone()),
worktree_id: ActiveValue::set(server.worktree_id.map(|id| id as i64)),
capabilities: ActiveValue::set(update.capabilities.clone()),
})
.on_conflict(
@@ -704,6 +705,7 @@ impl Database {
.update_columns([
language_server::Column::Name,
language_server::Column::Capabilities,
language_server::Column::WorktreeId,
])
.to_owned(),
)
@@ -1065,7 +1067,7 @@ impl Database {
server: proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
worktree_id: None,
worktree_id: language_server.worktree_id.map(|id| id as u64),
},
capabilities: language_server.capabilities,
})

View File

@@ -809,7 +809,7 @@ impl Database {
server: proto::LanguageServer {
id: language_server.id as u64,
name: language_server.name,
worktree_id: None,
worktree_id: language_server.worktree_id.map(|id| id as u64),
},
capabilities: language_server.capabilities,
})

View File

@@ -10,6 +10,7 @@ pub struct Model {
pub id: i64,
pub name: String,
pub capabilities: String,
pub worktree_id: Option<i64>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -476,7 +476,9 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
.add_message_handler(update_context);
.add_message_handler(update_context)
.add_request_handler(forward_mutating_project_request::<proto::ToggleLspLogs>)
.add_message_handler(broadcast_project_message_from_host::<proto::LanguageServerLog>);
Arc::new(server)
}

View File

@@ -369,7 +369,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -488,7 +488,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
.set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -615,7 +615,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(0, 0));
assert_eq!(params.range.end, lsp::Position::new(0, 0));
@@ -637,7 +637,7 @@ async fn test_collaborating_with_code_actions(
.set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.range.start, lsp::Position::new(1, 31));
assert_eq!(params.range.end, lsp::Position::new(1, 31));
@@ -649,7 +649,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -659,7 +659,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -721,7 +721,7 @@ async fn test_collaborating_with_code_actions(
changes: Some(
[
(
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(1, 22),
@@ -731,7 +731,7 @@ async fn test_collaborating_with_code_actions(
)],
),
(
lsp::Url::from_file_path(path!("/a/other.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(
lsp::Position::new(0, 0),
@@ -949,14 +949,14 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
changes: Some(
[
(
lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
"THREE".to_string(),
)],
),
(
lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(),
vec![
lsp::TextEdit::new(
lsp::Range::new(
@@ -1574,7 +1574,7 @@ async fn test_on_input_format_from_host_to_guest(
|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1717,7 +1717,7 @@ async fn test_on_input_format_from_guest_to_host(
.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
assert_eq!(
params.text_document_position.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.text_document_position.position,
@@ -1901,7 +1901,7 @@ async fn test_mutual_editor_inlay_hint_cache_update(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
Ok(Some(vec![lsp::InlayHint {
@@ -2151,7 +2151,7 @@ async fn test_inlay_hint_refresh_is_forwarded(
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
let character = if other_hints { 0 } else { 2 };
@@ -2332,7 +2332,7 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
requests_made.fetch_add(1, atomic::Ordering::Release);
Ok(vec![lsp::ColorInformation {
@@ -2621,11 +2621,11 @@ async fn test_lsp_pull_diagnostics(
let requests_made = closure_diagnostics_pulls_made.clone();
let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
async move {
let message = if lsp::Url::from_file_path(path!("/a/main.rs")).unwrap()
let message = if lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()
== params.text_document.uri
{
expected_pull_diagnostic_main_message.to_string()
} else if lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap()
} else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
== params.text_document.uri
{
expected_pull_diagnostic_lib_message.to_string()
@@ -2717,7 +2717,7 @@ async fn test_lsp_pull_diagnostics(
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2746,7 +2746,7 @@ async fn test_lsp_pull_diagnostics(
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2821,7 +2821,7 @@ async fn test_lsp_pull_diagnostics(
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
@@ -2842,7 +2842,7 @@ async fn test_lsp_pull_diagnostics(
);
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range {
start: lsp::Position {
@@ -2870,7 +2870,7 @@ async fn test_lsp_pull_diagnostics(
items: vec![
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -2902,7 +2902,7 @@ async fn test_lsp_pull_diagnostics(
),
lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report:
lsp::FullDocumentDiagnosticReport {
@@ -3051,7 +3051,7 @@ async fn test_lsp_pull_diagnostics(
lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
lsp::WorkspaceFullDocumentDiagnosticReport {
uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
version: None,
full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
result_id: Some(format!(
@@ -4040,7 +4040,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(params.position, lsp::Position::new(0, 0));
Ok(Some(ExpandedMacro {
@@ -4075,7 +4075,7 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|params, _| async move {
assert_eq!(
params.text_document.uri,
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
);
assert_eq!(
params.position,

View File

@@ -3072,12 +3072,7 @@ async fn test_git_status_sync(
.collect::<Vec<_>>();
assert_eq!(repos.len(), 1);
let repo = repos.into_iter().next().unwrap();
assert_eq!(
repo.read(cx)
.status_for_path(&file.into())
.map(|entry| entry.status),
status
);
assert_eq!(repo.read(cx).status_for_path(&file.into()), status);
}
project_local.read_with(cx_a, |project, cx| {
@@ -4075,7 +4070,7 @@ async fn test_collaborating_with_diagnostics(
.await;
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4095,7 +4090,7 @@ async fn test_collaborating_with_diagnostics(
.unwrap();
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::ERROR),
@@ -4169,7 +4164,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting more errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: vec![
lsp::Diagnostic {
@@ -4265,7 +4260,7 @@ async fn test_collaborating_with_diagnostics(
// Simulate a language server reporting no errors for a file.
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/a/a.rs")).unwrap(),
version: None,
diagnostics: Vec::new(),
},
@@ -4372,7 +4367,7 @@ async fn test_collaborating_with_lsp_progress_updates_and_diagnostics_ordering(
for file_name in file_names {
fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
&lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
uri: lsp::Uri::from_file_path(Path::new(path!("/test")).join(file_name)).unwrap(),
version: None,
diagnostics: vec![lsp::Diagnostic {
severity: Some(lsp::DiagnosticSeverity::WARNING),
@@ -4838,7 +4833,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))
@@ -4876,7 +4871,7 @@ async fn test_definition(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/dir-2/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
),
)))
@@ -4914,7 +4909,7 @@ async fn test_definition(
);
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/dir-2/c.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
),
)))
@@ -5049,15 +5044,15 @@ async fn test_references(
lsp_response_tx
.unbounded_send(Ok(Some(vec![
lsp::Location {
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
},
lsp::Location {
uri: lsp::Url::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir-1/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
},
lsp::Location {
uri: lsp::Url::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/root/dir-2/three.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
},
])))
@@ -5625,7 +5620,7 @@ async fn test_project_symbols(
lsp::SymbolInformation {
name: "TWO".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/code/crate-2/two.rs")).unwrap(),
range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
},
kind: lsp::SymbolKind::CONSTANT,
@@ -5737,7 +5732,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|_, _| async move {
Ok(Some(lsp::GotoDefinitionResponse::Scalar(
lsp::Location::new(
lsp::Url::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Uri::from_file_path(path!("/root/b.rs")).unwrap(),
lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
),
)))

View File

@@ -1101,7 +1101,7 @@ impl RandomizedTest for ProjectCollaborationTest {
files
.into_iter()
.map(|file| lsp::Location {
uri: lsp::Url::from_file_path(file).unwrap(),
uri: lsp::Uri::from_file_path(file).unwrap(),
range: Default::default(),
})
.collect(),

View File

@@ -26,7 +26,7 @@ use project::{
debugger::session::ThreadId,
lsp_store::{FormatTrigger, LspFormatTarget},
};
use remote::SshRemoteClient;
use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use rpc::proto;
use serde_json::json;
@@ -59,7 +59,7 @@ async fn test_sharing_an_ssh_remote_project(
.await;
// Set up project on remote FS
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -101,7 +101,7 @@ async fn test_sharing_an_ssh_remote_project(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/code/project1"), client_ssh, cx_a)
.await;
@@ -235,7 +235,7 @@ async fn test_ssh_collaboration_git_branches(
.await;
// Set up project on remote FS
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree("/project", serde_json::json!({ ".git":{} }))
@@ -268,7 +268,7 @@ async fn test_ssh_collaboration_git_branches(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, _) = client_a
.build_ssh_project("/project", client_ssh, cx_a)
.await;
@@ -420,7 +420,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
let buffer_text = "let one = \"two\"";
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
@@ -473,7 +473,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let (project_a, worktree_id) = client_a
.build_ssh_project(path!("/project"), client_ssh, cx_a)
.await;
@@ -602,7 +602,7 @@ async fn test_remote_server_debugger(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -633,7 +633,7 @@ async fn test_remote_server_debugger(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {
@@ -711,7 +711,7 @@ async fn test_slow_adapter_startup_retries(
release_channel::init(SemanticVersion::default(), cx);
dap_adapters::init(cx);
});
let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let (opts, server_ssh) = RemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
@@ -742,7 +742,7 @@ async fn test_slow_adapter_startup_retries(
)
});
let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await;
let client_ssh = RemoteClient::fake_client(opts, cx_a).await;
let mut server = TestServer::start(server_cx.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
cx_a.update(|cx| {

View File

@@ -26,7 +26,7 @@ use node_runtime::NodeRuntime;
use notifications::NotificationStore;
use parking_lot::Mutex;
use project::{Project, WorktreeId};
use remote::SshRemoteClient;
use remote::RemoteClient;
use rpc::{
RECEIVE_TIMEOUT,
proto::{self, ChannelRole},
@@ -765,11 +765,11 @@ impl TestClient {
pub async fn build_ssh_project(
&self,
root_path: impl AsRef<Path>,
ssh: Entity<SshRemoteClient>,
ssh: Entity<RemoteClient>,
cx: &mut TestAppContext,
) -> (Entity<Project>, WorktreeId) {
let project = cx.update(|cx| {
Project::ssh(
Project::remote(
ssh,
self.client().clone(),
self.app_state.node_runtime.clone(),

View File

@@ -3047,7 +3047,7 @@ impl Render for CollabPanel {
.on_action(cx.listener(CollabPanel::move_channel_down))
.track_focus(&self.focus_handle)
.size_full()
.child(if !self.client.status().borrow().is_connected() {
.child(if !self.client.status().borrow().is_or_was_connected() {
self.render_signed_out(cx)
} else {
self.render_signed_in(window, cx)

View File

@@ -66,5 +66,7 @@ fn notification_window_options(
app_id: Some(app_id.to_owned()),
window_min_size: None,
window_decorations: Some(WindowDecorations::Client),
tabbing_identifier: None,
..Default::default()
}
}

View File

@@ -1,10 +1,10 @@
use gpui::Pixels;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, SettingsUi)]
pub struct CollaborationPanelSettings {
pub button: bool,
pub dock: DockPosition,
@@ -20,7 +20,7 @@ pub enum ChatPanelButton {
WhenInCall,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, SettingsUi)]
pub struct ChatPanelSettings {
pub button: ChatPanelButton,
pub dock: DockPosition,
@@ -43,7 +43,7 @@ pub struct ChatPanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, SettingsUi)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
@@ -66,7 +66,7 @@ pub struct PanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `👋`.

View File

@@ -1,7 +1,10 @@
use anyhow::Result;
use db::{
define_connection, query,
sqlez::{bindable::Column, statement::Statement},
query,
sqlez::{
bindable::Column, domain::Domain, statement::Statement,
thread_safe_connection::ThreadSafeConnection,
},
sqlez_macros::sql,
};
use serde::{Deserialize, Serialize};
@@ -50,8 +53,11 @@ impl Column for SerializedCommandInvocation {
}
}
define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> =
&[sql!(
pub struct CommandPaletteDB(ThreadSafeConnection);
impl Domain for CommandPaletteDB {
const NAME: &str = stringify!(CommandPaletteDB);
const MIGRATIONS: &[&str] = &[sql!(
CREATE TABLE IF NOT EXISTS command_invocations(
id INTEGER PRIMARY KEY AUTOINCREMENT,
command_name TEXT NOT NULL,
@@ -59,7 +65,9 @@ define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()>
last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL
) STRICT;
)];
);
}
db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
impl CommandPaletteDB {
pub async fn write_command_invocation(

View File

@@ -197,7 +197,7 @@ impl Status {
}
struct RegisteredBuffer {
uri: lsp::Url,
uri: lsp::Uri,
language_id: String,
snapshot: BufferSnapshot,
snapshot_version: i32,
@@ -1108,9 +1108,9 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String {
.unwrap_or_else(|| "plaintext".to_string())
}
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Uri, ()> {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
lsp::Url::from_file_path(file.abs_path(cx))
lsp::Uri::from_file_path(file.abs_path(cx))
} else {
format!("buffer://{}", buffer.entity_id())
.parse()
@@ -1201,7 +1201,7 @@ mod tests {
let (copilot, mut lsp) = Copilot::fake(cx);
let buffer_1 = cx.new(|cx| Buffer::local("Hello", cx));
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
let buffer_1_uri: lsp::Uri = format!("buffer://{}", buffer_1.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
@@ -1219,7 +1219,7 @@ mod tests {
);
let buffer_2 = cx.new(|cx| Buffer::local("Goodbye", cx));
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
let buffer_2_uri: lsp::Uri = format!("buffer://{}", buffer_2.entity_id().as_u64())
.parse()
.unwrap();
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
@@ -1270,7 +1270,7 @@ mod tests {
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
}
);
let buffer_1_uri = lsp::Url::from_file_path(path!("/root/child/buffer-1")).unwrap();
let buffer_1_uri = lsp::Uri::from_file_path(path!("/root/child/buffer-1")).unwrap();
assert_eq!(
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await,

View File

@@ -62,12 +62,6 @@ impl CopilotChatConfiguration {
}
}
// Copilot's base model; defined by Microsoft in premium requests table
// This will be moved to the front of the Copilot model list, and will be used for
// 'fast' requests (e.g. title generation)
// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests
const DEFAULT_MODEL_ID: &str = "gpt-4.1";
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
@@ -101,22 +95,39 @@ where
Ok(models)
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Model {
billing: ModelBilling,
capabilities: ModelCapabilities,
id: String,
name: String,
policy: Option<ModelPolicy>,
vendor: ModelVendor,
is_chat_default: bool,
// The model with this value true is selected by VSCode copilot if a premium request limit is
// reached. Zed does not currently implement this behaviour
is_chat_fallback: bool,
model_picker_enabled: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
struct ModelBilling {
is_premium: bool,
multiplier: f64,
// List of plans a model is restricted to
// Field is not present if a model is available for all plans
#[serde(default)]
restricted_to: Option<Vec<String>>,
}
#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ModelCapabilities {
family: String,
#[serde(default)]
limits: ModelLimits,
supports: ModelSupportedFeatures,
#[serde(rename = "type")]
model_type: String,
}
#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -153,6 +164,8 @@ pub enum ModelVendor {
OpenAI,
Google,
Anthropic,
#[serde(rename = "xAI")]
XAI,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
@@ -602,6 +615,7 @@ async fn get_models(
.into_iter()
.filter(|model| {
model.model_picker_enabled
&& model.capabilities.model_type.as_str() == "chat"
&& model
.policy
.as_ref()
@@ -610,9 +624,7 @@ async fn get_models(
.dedup_by(|a, b| a.capabilities.family == b.capabilities.family)
.collect();
if let Some(default_model_position) =
models.iter().position(|model| model.id == DEFAULT_MODEL_ID)
{
if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) {
let default_model = models.remove(default_model_position);
models.insert(0, default_model);
}
@@ -630,7 +642,9 @@ async fn request_models(
.uri(models_url.as_ref())
.header("Authorization", format!("Bearer {}", api_token))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat");
.header("Copilot-Integration-Id", "vscode-chat")
.header("Editor-Version", "vscode/1.103.2")
.header("x-github-api-version", "2025-05-01");
let request = request_builder.body(AsyncBody::empty())?;
@@ -801,6 +815,10 @@ mod tests {
let json = r#"{
"data": [
{
"billing": {
"is_premium": false,
"multiplier": 0
},
"capabilities": {
"family": "gpt-4",
"limits": {
@@ -814,6 +832,8 @@ mod tests {
"type": "chat"
},
"id": "gpt-4",
"is_chat_default": false,
"is_chat_fallback": false,
"model_picker_enabled": false,
"name": "GPT 4",
"object": "model",
@@ -825,6 +845,16 @@ mod tests {
"some-unknown-field": 123
},
{
"billing": {
"is_premium": true,
"multiplier": 1,
"restricted_to": [
"pro",
"pro_plus",
"business",
"enterprise"
]
},
"capabilities": {
"family": "claude-3.7-sonnet",
"limits": {
@@ -848,6 +878,8 @@ mod tests {
"type": "chat"
},
"id": "claude-3.7-sonnet",
"is_chat_default": false,
"is_chat_fallback": false,
"model_picker_enabled": true,
"name": "Claude 3.7 Sonnet",
"object": "model",

View File

@@ -102,7 +102,7 @@ pub struct GetCompletionsDocument {
pub tab_size: u32,
pub indent_size: u32,
pub insert_spaces: bool,
pub uri: lsp::Url,
pub uri: lsp::Uri,
pub relative_path: String,
pub position: lsp::Position,
pub version: usize,

View File

@@ -2,9 +2,9 @@ use dap_types::SteppingGranularity;
use gpui::{App, Global};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsSources, SettingsUi};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum DebugPanelDockPosition {
Left,
@@ -12,12 +12,14 @@ pub enum DebugPanelDockPosition {
Right,
}
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
#[serde(default)]
#[settings_ui(group = "Debugger", path = "debugger")]
pub struct DebuggerSettings {
/// Determines the stepping granularity.
///
/// Default: line
#[settings_ui(skip)]
pub stepping_granularity: SteppingGranularity,
/// Whether the breakpoints should be reused across Zed sessions.
///

View File

@@ -234,6 +234,7 @@ impl PythonDebugAdapter {
.await
.map_err(|e| format!("{e:#?}"))?
.success();
if !did_succeed {
return Err("Failed to create base virtual environment".into());
}

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