Compare commits

..

190 Commits

Author SHA1 Message Date
Peter Tripp
88a37b04aa Up or Down 2025-08-01 16:23:32 -04:00
Peter Tripp
23dddba1e6 Upper controls. 2025-08-01 15:43:30 -04:00
Danilo Leal
faa45c53d7 onboarding: Add design adjustments (#35480)
Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-08-01 15:08:15 -03:00
Max Brunsfeld
b31f893408 Rasterize glyphs without D2D (#35376)
This allows debugging Zed with Renderdoc, and also fixes an issue where
glyphs' bounds were miscalculated for certain sizes and scale factors.

Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: Julia <julia@zed.dev>
Co-authored-by: Junkui Zhang <364772080@qq.com>
2025-08-01 19:46:09 +02:00
Antonio Scandurra
f888f3fc0b Start separating authentication from connection to collab (#35471)
This pull request should be idempotent, but lays the groundwork for
avoiding to connect to collab in order to interact with AI features
provided by Zed.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-08-01 17:37:38 +00:00
Finn Evers
b01d1872cc onboarding: Add the AI page (#35351)
This PR starts the work on the AI onboarding page as well as the
configuration modal

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
2025-08-01 10:43:59 -04:00
Ben Brandt
e5c6a596a9 agent_ui: More agent notifications (#35441)
Release Notes:

- N/A
2025-08-01 14:29:02 +00:00
Joseph T. Lyons
106aa0d9cc Add default binding to open settings profile selector (#35459)
Release Notes:

- N/A
2025-08-01 05:53:40 +00:00
Marshall Bowers
f7f90593ac inline_completion_button: Replace UserStore with CloudUserStore (#35456)
This PR replaces usages of the `UserStore` in the inline completion
button with the `CloudUserStore`.

Release Notes:

- N/A
2025-08-01 03:25:23 +00:00
Marshall Bowers
8be3f48f37 client: Remove unused subscription_period from UserStore (#35454)
This PR removes the `subscription_period` field from the `UserStore`, as
its usage has been replaced by the `CloudUserStore`.

Release Notes:

- N/A
2025-08-01 03:10:16 +00:00
Peter Tripp
76a8293cc6 editor_tests: Fix for potential race loading editor languages (#35453)
Fix for potential race when loading HTML and JS languages (JS is
slower). Wait for both to load before continue tests.
Observed failure on linux:
[job](https://github.com/zed-industries/zed/actions/runs/16662438526/job/47162345259)
as part of https://github.com/zed-industries/zed/pull/35436

```
    thread 'editor_tests::test_autoclose_with_embedded_language' panicked at crates/editor/src/editor_tests.rs:8724:8:
    assertion failed: `(left == right)`: unexpected buffer text

    Diff < left / right > :
     <body><>
         <script>
    <        var x = 1;<>
    >        var x = 1;<
         </script>
     </body><>
```

Inserted `<` incorrect gets paired bracket inserted `>`.
I believe because the JS language injection hasn't fully loaded.

Release Notes:

- N/A
2025-08-01 03:05:03 +00:00
Marshall Bowers
2315962e18 cloud_api_client: Add accept_terms_of_service method (#35452)
This PR adds an `accept_terms_of_service` method to the
`CloudApiClient`.

Release Notes:

- N/A
2025-08-01 02:50:38 +00:00
Marshall Bowers
f8673dacf5 ai_onboarding: Read the plan from the CloudUserStore (#35451)
This PR updates the AI onboarding to read the plan from the
`CloudUserStore` so that we don't need to connect to Collab.

Release Notes:

- N/A
2025-08-01 02:08:21 +00:00
Marshall Bowers
72d354de6c Update Agent panel to work with CloudUserStore (#35436)
This PR updates the Agent panel to work with the `CloudUserStore`
instead of the `UserStore`, reducing its reliance on being connected to
Collab to function.

Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-08-01 01:44:43 +00:00
Marshall Bowers
09b93caa9b Rework authentication for local Cloud/Collab development (#35450)
This PR reworks authentication for developing Zed against a local
version of Cloud and/or Collab.

You will still connect the same way—using the `zed-local` script—but
will need to be running an instance of Cloud locally.

Release Notes:

- N/A
2025-08-01 00:55:17 +00:00
Cole Miller
7c169fc9b5 debugger: Send initialized event from fake server at a more realistic time (#35446)
The spec says:

> ⬅️ Initialized Event
> This event indicates that the debug adapter is ready to accept
configuration requests (e.g. setBreakpoints, setExceptionBreakpoints).
>
> A debug adapter is expected to send this event when it is ready to
accept configuration requests (but not before the initialize request has
finished).

Previously in tests, `intercept_debug_sessions` was just spawning off a
background task to send the event after setting up the client, so the
event wasn't actually synchronized with the flow of messages in the way
the spec says it should be. This PR makes it so that the `FakeTransport`
injects the event right after a successful response to the initialize
request, and doesn't send it otherwise.

Release Notes:

- N/A
2025-07-31 19:45:02 -04:00
Mikayla Maki
2b36d4ec94 Add a field to MultiLSPQuery span showing the current request (#35372)
Release Notes:

- N/A
2025-07-31 22:40:19 +00:00
Peter Tripp
4a82b6c5ee jetbrains: Unmap cmd-k in Jetbrains keymap (#35443)
This only works after a delay in most situations because of the all
chorded `cmd-k` mappings in the so disable them for now.

Reported by @jer-k:
https://x.com/J_Kreutzbender/status/1951033355434336606

Release Notes:

- Undo mapping of `cmd-k` for Git Panel in default Jetbrains keymap
(thanks [@jer-k](https://github.com/jer-k))
2025-07-31 18:29:51 -04:00
Joseph T. Lyons
5feb759c20 Additions for settings profile selector (#35439)
- Added profile selector to `zed > settings` submenu.
- Added examples to the `default.json` docs.
- Reduced length of the setting description that shows on autocomplete,
since it was cutoff in the autocomplete popover.


Release Notes:

- N/A
2025-07-31 22:20:35 +00:00
Marshall Bowers
410348deb0 Acquire LLM token from Cloud instead of Collab for Edit Predictions (#35431)
This PR updates the Zed Edit Prediction provider to acquire the LLM
token from Cloud instead of Collab to allow using Edit Predictions even
when disconnected from or unable to connect to the Collab server.

Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-31 22:12:04 +00:00
Mikayla Maki
8e7f1899e1 Revert "Increase the number of parallel request handlers per connection" (#35435)
Reverts zed-industries/zed#35046

This made the problem worse ;-;

Release Notes:

- N/A
2025-07-31 17:31:29 -04:00
Marshall Bowers
aea1d48184 cloud_api_client: Add create_llm_token method (#35428)
This PR adds a `create_llm_token` method to the `CloudApiClient`.

Release Notes:

- N/A
2025-07-31 21:01:21 +00:00
Ben Kunkle
c946b98ea1 onboarding: Expand power of theme selector (#35421)
Closes #ISSUE

The behavior of the theme selector is documented above the function,
copied here for reference:
```rust
/// separates theme "mode" ("dark" | "light" | "system") into two separate states
/// - appearance = "dark" | "light"
/// - "system" true/false
/// when system selected:
///  - toggling between light and dark does not change theme.mode, just which variant will be changed
/// when system not selected:
///  - toggling between light and dark does change theme.mode
/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme,
///
/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not
/// it does not support setting theme to a static value
```

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-31 16:21:58 -04:00
Anthony Eid
c6947ee4f0 onboarding ui: Add theme preview tiles and button functionality to basic page (#35413)
This PR polishes and adds functionality to the onboarding UI with a
focus on the basic page. It added theme preview tiles, got the Vim,
telemetry, crash reporting, and sign-in button working.

The theme preview component was moved to the UI crate and it now can
have a click handler on it.

Finally, this commit also changed `client::User.github_login` and
`client::UserStore.by_github_login` to use `SharedStrings` instead of
`Strings`. This change was made because user.github_login was cloned in
several areas including the UI, and was cast to a shared string in some
cases too.

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-07-31 18:40:41 +00:00
Marshall Bowers
b59f992928 cloud_api_types: Add types for POST /client/llm_tokens endpoint (#35420)
This PR adds some types for the new `POST /client/llm_tokens` endpoint.

Release Notes:

- N/A

Co-authored-by: Richard <richard@zed.dev>
2025-07-31 18:00:29 +00:00
Joseph T. Lyons
0a21b845fa Tighten up settings profile selector modal width (#35419)
Release Notes:

- N/A
2025-07-31 17:31:12 +00:00
Kirill Bulatov
6a8be1714e Fix panic with completion ranges and autoclose regions interop (#35408)
As reported [in
Discord](https://discord.com/channels/869392257814519848/1106226198494859355/1398470747227426948)
C projects with `"` as "brackets" that autoclose, may invoke panics when
edited at the end of the file.

With a single selection-caret (`ˇ`), at the end of the file,
```c
ifndef BAR_H
#define BAR_H

#include <stdbool.h>

int fn_branch(bool do_branch1, bool do_branch2);

#endif // BAR_H
#include"ˇ"
```
gets an LSP response from clangd
```jsonc
{
  "filterText": "AGL/",
  "insertText": "AGL/",
  "insertTextFormat": 1,
  "kind": 17,
  "label": " AGL/",
  "labelDetails": {},
  "score": 0.78725427389144897,
  "sortText": "40b67681AGL/",
  "textEdit": {
    "newText": "AGL/",
    "range": { "end": { "character": 11, "line": 8 }, "start": { "character": 10, "line": 8 } }
  }
}
```

which replaces `"` after the caret (character/column 11, 0-indexed).
This is reasonable, as regular follow-up (proposed in further
completions), is a suffix + a closing `"`:

<img width="842" height="259" alt="image"
src="https://github.com/user-attachments/assets/ea56f621-7008-4ce2-99ba-87344ddf33d2"
/>

Yet when Zed handles user input of `"`, it panics due to multiple
reasons:

* after applying any snippet text edit, Zed did a selection change:
5537987630/crates/editor/src/editor.rs (L9539-L9545)
which caused eventual autoclose region invalidation:
5537987630/crates/editor/src/editor.rs (L2970)

This covers all cases that insert the `include""` text.

* after applying any user input and "plain" text edit, Zed did not
invalidate any autoclose regions at all, relying on the "bracket" (which
includes `"`) autoclose logic to rule edge cases out

* bracket autoclose logic detects previous `"` and considers the new
user input as a valid closure, hence no autoclose region needed.
But there is an autoclose bracket data after the plaintext completion
insertion (`AGL/`) really, and it's not invalidated after `"` handling

* in addition to that, `Anchor::is_valid` method in `text` panicked, and
required `fn try_fragment_id_for_anchor` to handle "pointing at odd,
after the end of the file, offset" cases as `false`

A test reproducing the feedback and 2 fixes added: proper, autoclose
region invalidation call which required the invalidation logic tweaked a
bit, and "superficial", "do not apply bad selections that cause panics"
fix in the editor to be more robust

Release Notes:

- Fixed panic with completion ranges and autoclose regions interop

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-07-31 16:18:26 +00:00
Cole Miller
a2aea00253 Bump livekit-rust-sdks with another attempt to fix build failures (#35344)
Includes https://github.com/zed-industries/livekit-rust-sdks/pull/7

Release Notes:

- N/A
2025-07-31 11:39:46 -04:00
张小白
98c66eddb8 windows: Don't create directx device with debug flag when debug layer is missing (#35405)
Release Notes:

- N/A
2025-07-31 14:42:44 +00:00
Marshall Bowers
558bbfffae title_bar: Show the plan from the CloudUserStore (#35401)
This PR updates the user menu in the title bar to show the plan from the
`CloudUserStore` instead of the `UserStore`.

We're still leveraging the RPC connection to listen for `UpdateUserPlan`
messages so that we can get live-updates from the server, but we are
merely using this as a signal to re-fetch the information from Cloud.

Release Notes:

- N/A
2025-07-31 13:56:53 +00:00
Smit Barmase
89ed0b9601 workspace: Fix multiple remote projects not restoring on reconnect or restart and not visible in recent projects (#35398)
Closes #33787

We were not updating SSH paths after initial project was created. Now we
update paths when worktrees are added/removed and serialize these
updated paths. This is separate from workspace because unlike local
paths, SSH paths are not part of the workspace table, but the SSH table
instead. We don't need to update SSH paths every time we serialize the
workspace.

<img width="400"
src="https://github.com/user-attachments/assets/9e1a9893-e08e-4ecf-8dab-1e9befced58b"
/>

Release Notes:

- Fixed issue where multiple remote folders in a project were lost on
reconnect, not restored on restart, and not visible in recent projects.
2025-07-31 16:32:31 +05:30
Julia Ryan
4b9334b910 Fix vim cw at end of words (#35300)
Fixes #35269

Release Notes:

- N/A
2025-07-31 03:48:36 -07:00
Joseph T. Lyons
47af878ebb Do not sort settings profiles (#35389)
After playing with this for a bit, I realize it does not feel good to
not have control over the order of profiles. I find myself wanting to
group similar profiles together and not being able to.

Release Notes:

- N/A
2025-07-31 07:34:35 +00:00
Danilo Leal
5488398986 onboarding: Refine page and component designs (#35387)
Includes adding new variants to the Dropdown and Numeric Stepper
components.

Release Notes:

- N/A
2025-07-31 05:32:18 +00:00
Marshall Bowers
b1a7993544 cloud_api_types: Add more data to the GetAuthenticatedUserResponse (#35384)
This PR adds more data to the `GetAuthenticatedUserResponse`.

We now return more information about the authenticated user, as well as
their plan information.

Release Notes:

- N/A
2025-07-30 23:38:51 -04:00
Marshall Bowers
b90fd4287f client: Don't fetch the authenticated user once we have them (#35385)
This PR makes it so we don't keep fetching the authenticated user once
we have them.

Release Notes:

- N/A
2025-07-31 03:37:02 +00:00
Ben Kunkle
e1e2775b80 docs: Run lychee link check on generated docs output (#35381)
Closes #ISSUE

Following #35310, . This PR makes it so the lychee link check is ran
before building the docs on the md files to catch basic errors, and then
after building on the html output to catch generation errors, including
regressions like the one #35380 fixes.

Release Notes:

- N/A
2025-07-31 02:01:40 +00:00
Joseph T. Lyons
ed104ec5e0 Ensure settings are being adjusted via settings profile selector (#35382)
This PR just pins down the behavior of the settings profile selector by
checking a single setting, `buffer_font_size`, as options in the
selector are changed / selected.

Release Notes:

- N/A
2025-07-31 01:52:02 +00:00
Kainoa Kanter
67a491df50 Use outlined bolt icon for the LSP tool (#35373)
| Before | After |
|--------|--------|
| <img width="266" height="67" alt="image"
src="https://github.com/user-attachments/assets/bbfc75b6-6747-4eb1-ab94-ab098eba5335"
/> | <img width="266" height="67" alt="image"
src="https://github.com/user-attachments/assets/4631be9d-3d5e-4eb6-bf2f-596403fdf014"
/> |

Release Notes:

- Changed the icon of the language servers entry in the status bar.
2025-07-30 21:37:10 -04:00
Marshall Bowers
f003036aec docs: Pin mdbook to v0.4.40 (#35380)
This PR pins `mdbook` to v0.4.40 to fix an issue with sidebar links
having some of their path segments duplicated (e.g.,
`http://localhost:3000/extensions/extensions/developing-extensions.html`.

For reference:

-
https://zed-industries.slack.com/archives/C04S5TU0RSN/p1745439470378339?thread_ts=1745428671.190059&cid=C04S5TU0RSN
-
https://zed-industries.slack.com/archives/C04S5TU0RSN/p1753922478290399

Release Notes:

- N/A
2025-07-31 01:34:26 +00:00
Marshall Bowers
fbc784d323 Use the user from the CloudUserStore to drive the user menu (#35375)
This PR updates the user menu in the title bar to base the "signed in"
state on the user in the `CloudUserStore` rather than the `UserStore`.

This makes it possible to be signed-in—at least, as far as the user menu
is concerned—even when disconnected from Collab.

Release Notes:

- N/A
2025-07-30 20:31:22 -04:00
Piotr Osiewicz
296bb66b65 chore: Move a few more tasks into background_spawn (#35374)
Release Notes:

- N/A
2025-07-30 23:56:47 +00:00
Marshall Bowers
bb1a7ccbba client: Add CloudUserStore (#35370)
This PR adds a new `CloudUserStore` for storing information about the
user retrieved from Cloud instead of Collab.

Release Notes:

- N/A
2025-07-30 18:43:10 -04:00
Marshall Bowers
289f420504 Sort crate members in Cargo.toml (#35371)
This PR sorts the crate members in the `Cargo.toml` file, as they had
gotten unsorted.

Release Notes:

- N/A
2025-07-30 22:35:17 +00:00
张小白
15ad986329 windows: Port to DirectX 11 (#34374)
Closes #16713
Closes #19739
Closes #33191
Closes #26692
Closes #17374
Closes #35077
Closes https://github.com/zed-industries/zed/issues/35205
Closes https://github.com/zed-industries/zed/issues/35262


Compared to the current Vulkan implementation, this PR brings several
improvements:

- Fewer weird bugs
- Better hardware compatibility
- VSync support
- More accurate colors
- Lower memory usage
- Graceful handling of device loss

---

**TODO:**

- [x] Don’t use AGS binaries directly
- [ ] The message loop is using too much CPU when ths app is idle
- [x] There’s a
[bug](https://github.com/zed-industries/zed/issues/33191#issuecomment-3109306630)
in how `Path` is being rendered.

---

Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-07-30 15:27:58 -07:00
Finn Evers
0d9715325c docs: Add section about terminal contrast adjustments (#35369)
Closes #35146

This change adds documentation for the `terminal.minimum_contrast`
setting to the docs as we've had a lot of reports regarding the contrast
adjustments, yet are missing proper documentation (aside from that in
the `defaults.json`) for it.

Release Notes:

- N/A
2025-07-31 00:19:56 +02:00
Joseph T. Lyons
5ef5f3c5ca Introduce settings profiles (#35339)
Settings Profiles

- [X] Allow profiles to be defined, where each profile can be any of
Zed's settings
    - [X] Autocompletion of all settings
    - [X] Errors on invalid keys
- [X] Action brings up modal that shows user-defined profiles
- [X] Alphabetize profiles
- [X] Ability to filter down via keyboard, and navigate via arrow up and
down
- [X] Auto select Disabled option by default (first in list, after
alphabetizing user-defined profiles)
- [X] Automatically select active profile on next picker summoning
- [X] Persist settings until toggled off
- [X] Show live preview as you select from the profile picker
- [X] Tweaking a setting, while in a profile, updates the profile live
- [X] Make sure actions that live update Zed, such as `cmd-0`, `cmd-+`,
and `cmd--`, work while in a profile
- [X] Add a test to track state

Release Notes:

- Added the ability to configure settings profiles, via the "profiles"
key. Example:

```json
{
  "profiles": {
    "Streaming": {
      "agent_font_size": 20,
      "buffer_font_size": 20,
      "theme": "One Light",
      "ui_font_size": 20
    }
  }
}
```

To set a profile, use `settings profile selector: toggle`
2025-07-30 21:48:24 +00:00
Anthony Eid
2d4afd2119 Polish onboarding page (#35367)
- Added borders to the numeric stepper.
- Changed the hover mouse style for SwitchField.
- Made the edit page toggle buttons more responsive.

Release Notes:

- N/A
2025-07-30 20:35:21 +00:00
Ben Kunkle
afcb8f2a3f onboarding: Wire up settings import (#35366)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-30 20:09:11 +00:00
Smit Barmase
cdce3b3620 linux: Fix caps lock not working consistently for certain X11 systems (#35361)
Closes #35316

Bug in https://github.com/zed-industries/zed/pull/34514

Turns out you are not supposed to call `update_key` for modifiers on
`KeyPress`/`KeyRelease`, as modifiers are already updated in
`XkbStateNotify` events. Not sure why this only causes issues on a few
systems and works on others.

Tested on Ubuntu 24.04.2 LTS (initial bug) and Kubuntu 25.04 (worked
fine before too).

Release Notes:

- Fixed an issue where caps lock stopped working consistently on some
Linux X11 systems.
2025-07-31 00:56:36 +05:30
Marshall Bowers
bc6bb42745 Add cloud_api_client and cloud_api_types crates (#35357)
This PR adds two new crates for interacting with Cloud:

- `cloud_api_client` - The client that will be used to talk to Cloud.
- `cloud_api_types` - The types for the Cloud API that are shared
between Zed and Cloud.

Release Notes:

- N/A
2025-07-30 18:57:51 +00:00
Marshall Bowers
7695c4b82e collab: Temporarily add back GET /user endpoint for local development (#35358)
This PR temporarily adds back the `GET /user` endpoint to Collab since
we're still using it for local development.

Will remove it again once we update the local development process to
leverage Cloud.

Release Notes:

- N/A
2025-07-30 18:54:44 +00:00
Smit Barmase
794ade8b6d ui_prompt: Fix prompt dialog is hard to see on large screen (#35348)
Closes #18516

Release Notes:

- Improved visibility of prompt dialog on Linux by dimming the
background.
2025-07-30 23:03:53 +05:30
Smit Barmase
f4bd524d7f ui_prompt: Fix copy version number from About Zed (#35346)
Closes #29361

Release Notes:

- Fixed not selectable version number in About Zed prompt on Linux.
2025-07-30 22:51:59 +05:30
Joseph T. Lyons
9d82e148de Bump Zed to v0.199 (#35343)
Release Notes:

-N/A
2025-07-30 16:53:53 +00:00
Finn Evers
f8d1062484 onboarding: Fix keybindings showing up after a delay (#35342)
This fixes an issue where keybinds would only show up after a delay on
the welcome page upon re-opening it. It also binds one of the buttons to
the corresponding action.

Release Notes:

- N/A
2025-07-30 16:18:14 +00:00
Antonio Scandurra
45af1fcc2f Always double reconnection delay and add jitter (#35337)
Previously, we would pick an exponent between 0.5 and 2.5, which would
cause a lot of clients to try reconnecting in rapid succession,
overwhelming the server as a result.

This pull request always doubles the previous delay and introduces a
jitter that can, at most, double it.

As part of this, we're also increasing the maximum reconnection delay
from 10s to 30s: this gives us more space to spread out the reconnection
requests.

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-07-30 15:34:09 +00:00
Peter Tripp
0aea5acc68 Fix Windows CI logic (#35335)
Fixes unintentional change in
https://github.com/zed-industries/zed/pull/35204

Release Notes:

- N/A
2025-07-30 11:01:20 -04:00
Kirill Bulatov
4d66d967f2 Revert "gpui: Implement support for wlr layer shell (#32651)" (#35331)
This reverts commit c110f78015.

On Linux Wayland, that causes a panic:

```
already mutably borrowed: BorrowError
zed::reliability::init_panic_hook::{{closure}}::h276cc55bf0717738+165677654
std::panicking::rust_panic_with_hook::h409da73ddef13937+139331443
std::panicking::begin_panic_handler::{{closure}}::h159b61b27f96a9c2+139330666
std::sys::backtrace::__rust_end_short_backtrace::h5b56844d75e766fc+139314825
__rustc[4794b31dd7191200]::rust_begin_unwind+139329805
core::panicking::panic_fmt::hc8737e8cca20a7c8+9934576
core::cell::panic_already_mutably_borrowed::h95c7d326eb19a92a+9934403
<gpui::platform::linux::wayland::window::WaylandWindow as gpui::platform::PlatformWindow>::set_app_id::hfa7deae0be264f60+10621600
gpui::window::Window::new::h6505f6042d99702f+80424235
gpui::app::async_context::AsyncApp::open_window::h62ef8f80789a0af2+159117345
workspace::Workspace::new_local::{{closure}}::{{closure}}::h4d786ba393f391b5+160720110
gpui::app::App::spawn::{{closure}}::haf6a6ef0f9bab21c+159294806
async_task::raw::RawTask<F,T,S,M>::run::h9e5f668e091fddff+158375501
<gpui::platform::linux::wayland::client::WaylandClient as gpui::platform::linux::platform::LinuxClient>::run::h69e40feabd97f1bb+79906738
gpui::platform::linux::platform::<impl gpui::platform::Platform for P>::run::hd80e5b2da41c7d0a+79758141
gpui::app::Application::run::h9136595e7346a2c9+163935333
zed::main::h83f7ef86a32dbbfd+165755480
std::sys::backtrace::__rust_begin_short_backtrace::hb6da6fe5454d7688+168421891
std::rt::lang_start::{{closure}}::h51a50d6423746d5f+168421865
std::rt::lang_start_internal::ha8ef919ae4984948+139244369
main+168421964
__libc_start_call_main+29344125649354
__libc_start_main_impl+29344125649547
_start+12961358
```


Release Notes:

- N/A
2025-07-30 13:36:22 +00:00
Kirill Bulatov
93e6b01486 Actually disable ai for now (#35327)
Closes https://github.com/zed-industries/zed/issues/35325

* removes Supermaven actions
* removes copilot-related action
* stops re-enabling edit predictions when disabled

Release Notes:

- N/A
2025-07-30 13:10:05 +00:00
Danilo Leal
00725273e4 agent: Rename "open configuration" action to "open settings" (#35329)
"Settings" is the terminology we use in the agent panel, thus having the
action use "configuration" makes it harder for folks to find this either
via the command palette or the keybinding editor UI in case they'd like
to change it.

Release Notes:

- agent: Renamed the "open configuration" action to "open settings" for
better discoverability and consistency
2025-07-30 09:55:13 -03:00
Piotr Osiewicz
c22fa9adee chore: Move a bunch of foreground tasks into background (#35322)
Closes #ISSUE

Release Notes:

- N/A
2025-07-30 10:29:03 +00:00
Kirill Bulatov
49b75e9e93 Kb/wasm panics (#35319)
Follow-up of https://github.com/zed-industries/zed/pull/34208
Closes https://github.com/zed-industries/zed/issues/35185

Previous code assumed that extensions' language server wrappers may leak
only in static data (e.g. fields that were not cleared on deinit), but
we seem to have a race that breaks this assumption.

1. We do clean `all_lsp_adapters` field after
https://github.com/zed-industries/zed/pull/34334 and it's called for
every extension that is unregistered.
2. `LspStore::maintain_workspace_config` ->
`LspStore::refresh_workspace_configurations` chain is triggered
independently, apparently on `ToolchainStoreEvent::ToolchainActivated`
event which means somewhere behind there's potentially a Python code
that gets executed to activate the toolchian, making
`refresh_workspace_configurations` start timings unpredictable.
3. Seems that toolchain activation overlaps with plugin reload, as 
`2025-07-28T12:16:19+03:00 INFO [extension_host] extensions updated.
loading 0, reloading 1, unloading 0` suggests in the issue logs.

The plugin reload seem to happen faster than workspace configuration
refresh in


c65da547c9/crates/project/src/lsp_store.rs (L7426-L7456)

as the language servers are just starting and take extra time to respond
to the notification.

At least one of the `.clone()`d `adapter`s there is the adapter that got
removed during plugin reload and has its channel closed, which causes a
panic later.

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

A good fix would be to re-architect the workspace refresh approach, same
as other accesses to the language server collections.
One way could be to use `Weak`-based structures instead, as definitely
the extension server data belongs to extension, not the `LspStore`.
This is quite a large undertaking near the extension core though, so is
not done yet.

Currently, to stop the excessive panics, no more `.expect` is done on
the channel result, as indeed, it now can be closed very dynamically.
This will result in more errors (and backtraces, presumably) printed in
the logs and no panics.

More logging and comments are added, and workspace querying is replaced
to the concurrent one: no need to wait until a previous server had
processed the notification to send the same to the next one.

Release Notes:

- Fixed warm-related panic happening during startup
2025-07-30 09:18:26 +00:00
Marshall Bowers
7be1f2418d Replace zed_llm_client with cloud_llm_client (#35309)
This PR replaces the usage of the `zed_llm_client` with the
`cloud_llm_client`.

It was ported into this repo in #35307.

Release Notes:

- N/A
2025-07-30 00:09:14 +00:00
Mikayla Maki
17a0179f0a Stop caching needlessly (#35308)
Release Notes:

- N/A
2025-07-29 23:38:06 +00:00
Marshall Bowers
b8f3a9101c Add cloud_llm_client crate (#35307)
This PR adds a `cloud_llm_client` crate to take the place of the
`zed_llm_client`.

Release Notes:

- N/A
2025-07-29 23:30:45 +00:00
Ben Kunkle
3824751e61 Add meta description tag to docs pages (#35112)
Closes #ISSUE

Adds basic frontmatter support to `.md` files in docs. The only
supported keys currently are `description` which becomes a `<meta
name="description" contents="...">` tag, and `title` which becomes a
normal `title` tag, with the title contents prefixed with the subject of
the file.

An example of the syntax can be found in `git.md`, as well as below

```md
---
title: Some more detailed title for this page
description: A page-specific description
---

# Editor
```

The above will be transformed into (with non-relevant tags removed)

```html
<head>
    <title>Editor | Some more detailed title for this page</title>
    <meta name="description" contents="A page-specific description">
</head>
<body>
<h1>Editor</h1>
</body>
```

If no front-matter is provided, or If one or both keys aren't provided,
the title and description will be set based on the `default-title` and
`default-description` keys in `book.toml` respectively.

## Implementation details

Unfortunately, `mdbook` does not support post-processing like it does
pre-processing, and only supports defining one description to put in the
meta tag per book rather than per file. So in order to apply
post-processing (necessary to modify the html head tags) the global book
description is set to a marker value `#description#` and the html
renderer is replaced with a sub-command of `docs_preprocessor` that
wraps the builtin `html` renderer and applies post-processing to the
`html` files, replacing the marker value and the `<title>(.*)</title>`
with the contents of the front-matter if there is one.

## Known limitations

The front-matter parsing is extremely simple, which avoids needing to
take on an additional dependency, or implement full yaml parsing.

* Double quotes and multi-line values are not supported, i.e. Keys and
values must be entirely on the same line, with no double quotes around
the value.

The following will not work:

```md
---
title: Some
 Multi-line
 Title
---
```

* The front-matter must be at the top of the file, with only white-space
preceding it

* The contents of the title and description will not be html-escaped.
They should be simple ascii text with no unicode or emoji characters

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Katie Greer <katie@zed.dev>
2025-07-29 23:01:03 +00:00
Finn Evers
57766199cf ui: Clean up toggle button group component (#35303)
This change cleans up the toggle button component a bit by utilizing
const parameters instead and also removes some clones by consuming the
values where possible instead.

Release Notes:

- N/A
2025-07-30 00:47:04 +02:00
Finn Evers
0be83f1c67 emmet: Bump to 0.0.4 (#35305)
This PR bumps the emmet extension to version 0.0.4. 

Includes:
- https://github.com/zed-industries/zed/pull/33865
- https://github.com/zed-industries/zed/pull/32208
- https://github.com/zed-industries/zed/pull/15177

Note that this intentionally does NOT include a change in the
`extension.toml`: The version was bumped incorrectly once in
https://github.com/zed-industries/zed/pull/32208 in both the
`extension.toml` as well as the `Cargo.lock` but not in the
`Cargo.toml`. After that,
https://github.com/zed-industries/zed/pull/33667 only removed the
changes in the `Cargo.lock` but didn't revert the change in the
`extension.toml` file. Hence, the version in the `extension.toml` is
already at `0.0.4`

Release Notes:

- N/A
2025-07-29 22:46:17 +00:00
Marshall Bowers
f0927faf61 collab: Add kill switches for syncing data to and from Stripe (#35304)
This PR adds two kill switches for syncing data to and from Stripe using
Collab.

The `cloud-stripe-events-polling` and `cloud-stripe-usage-meters-sync`
feature flags control whether we use Cloud for polling Stripe events and
updating Stripe meters, respectively.

When we're ready to hand off the syncing to Cloud we can enable the
feature flag to do so.

Release Notes:

- N/A
2025-07-29 22:45:00 +00:00
Marshall Bowers
d2d116cb02 collab: Remove GET /user endpoint (#35301)
This PR removes the `GET /user` endpoint, as it has been moved to
`cloud.zed.dev`.

Release Notes:

- N/A
2025-07-29 22:34:04 +00:00
Ben Kunkle
9f69b53869 keymap_ui: Additional cleanup (#35299)
Closes #ISSUE

Additional cleanup and testing for the keystroke input including
- Focused testing of the "previous modifiers" logic in search mode
- Not merging unmodified keystrokes into previous modifier only bindings
(extension of #35208)
- Fixing a bug where input would overflow in search mode when entering
only modifiers
- Additional testing logic to ensure keystrokes updated events are
always emitted correctly

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-29 18:04:00 -04:00
Anthony Eid
48e085a523 onboarding ui: Add editing page to onboarding page (#35298)
I added buttons for inlay values, showing the mini map, git blame, and
controlling the UI/Editor Font/Font size. The only thing left for this
page is some UI clean up and adding buttons for setting import from
VSCode/cursor.

I also added Numeric Stepper as a component preview.

Current state:
<img width="1085" height="585" alt="image"
src="https://github.com/user-attachments/assets/230df474-da81-4810-ba64-05673896d119"
/>


Release Notes:

- N/A
2025-07-29 21:54:58 +00:00
marius851000
3378f02b7e Fix link to panic location on GitHub (#35162)
I had a panic, and it reported


``24c2a465bb/src/crates/assistant_tools/src/edit_agent.rs (L686)
(may not be uploaded, line may be incorrect if files modified)``

The `/src` part seems superfluous, and result in a link that don’t work
(unlike
`24c2a465bb/src/crates/assistant_tools/src/edit_agent.rs (L686)`).
I don’t know why it originally worked (of if it even actually originally
worked properly), but there seems to be no reason to keep that `/src`.

Release Notes:

- N/A
2025-07-29 17:45:46 -04:00
Ridan Vandenbergh
c110f78015 gpui: Implement support for wlr layer shell (#32651)
I was interested in potentially using gpui for a hobby project, but
needed [layer
shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1)
support for it. Turns out gpui's (excellent!) architecture made that
super easy to implement, so I went ahead and did it.

Layer shell is a window role used for notification windows, lock
screens, docks, backgrounds, etc. Supporting it in gpui opens the door
to implementing applications like that using the framework.

If this turns out interesting enough to merge - I'm also happy to
provide a follow-up PR (when I have the time to) to implement some of
the desirable window options for layer shell surfaces, such as:
- namespace (currently always `""`)
- keyboard interactivity (currently always `OnDemand`, which mimics
normal keyboard interactivity)
- anchor, exclusive zone, margins
- popups

Release Notes:

- Added support for wayland layer shell surfaces in gpui

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-07-29 21:26:30 +00:00
Ben Kunkle
85b712c04e keymap_ui: Clear close keystroke capture on timeout (#35289)
Closes #ISSUE

Introduces a mechanism whereby keystrokes that have a post-fix which
matches the prefix of the stop recording binding can still be entered.
The solution is to introduce a (as of right now) 300ms timeout before
the close keystroke state is wiped.

Previously, with the default stop recording binding `esc esc esc`,
searching or entering a binding ending in esc was not possible without
using the mouse. `e.g.` entering keystroke `ctrl-g esc` and then
attempting to hit `esc` three times would stop recording on the
penultimate `esc` press and the final `esc` would not be intercepted.
Now with the timeout, it is possible to enter `ctrl-g esc`, pause for a
moment, then hit `esc esc esc` and end the recording with the keystroke
input state being `ctrl-g esc`.

I arrived at 300ms for this delay as it was long enough that I didn't
run into it very often when trying to escape, but short enough that a
natural pause will almost always work as expected.

Release Notes:

- Keymap Editor: Added a short timeout to the stop recording keybind
handling in the keystroke input, so that it is now possible using the
default bindings as an example (custom bindings should work as well) to
search for/enter a binding ending with `escape` (with no modifier),
pause for a moment, then hit `escape escape escape` to stop recording
and search for/enter a keystroke ending with `escape`.
2025-07-29 17:24:57 -04:00
Daniel Sauble
5fa212183a Fix animations in the component preview (#33673)
Fixes #33869

The Animation page in the Component Preview had a few issues.

* The animations only ran once, so you couldn't watch animations below
the fold.
* The offset math was wrong, so some animated elements were rendered
outside of their parent container.
* The "animate in from right" elements were defined with an initial
`.left()` offset, which overrode the animation behavior.

I made fixes to address these issues. In particular, every time you
click the active list item, it renders the preview again (which causes
the animations to run again).

Before:


https://github.com/user-attachments/assets/a1fa2e3f-653c-4b83-a6ed-c55ca9c78ad4

After:


https://github.com/user-attachments/assets/3623bbbc-9047-4443-b7f3-96bd92f582bf

Release Notes:

- N/A
2025-07-29 14:22:53 -07:00
David Kleingeld
1501ae0013 Upgrade rodio to 0.21 (#34368)
Hi all,

We just released [Rodio
0.21](https://github.com/RustAudio/rodio/blob/master/CHANGELOG.md)
🥳 with quite some breaking changes. This should take care
of those for zed. I tested it by hopping in and out some of the zed
channels, sound seems to still work.

Given zed uses tracing I also took the liberty of enabling the tracing
feature for rodio.

edit:
We changed the default wav decoder from hound to symphonia. The latter
has a slightly more restrictive license however that should be no issue
here (as the audio crate uses the GPL)

Release Notes:

- N/A
2025-07-29 13:24:34 -07:00
Joseph T. Lyons
3973142324 Adjust Zed badge (#35294)
- Make right side background white
- Fix Zed casing

Release Notes:

- N/A
2025-07-29 15:09:31 -04:00
Cole Miller
7878eacc73 python: Use a single workspace folder for basedpyright (#35292)
Treat the new basedpyright adapter the same as pyright was treated in
#35243.

Release Notes:

- N/A
2025-07-29 19:00:41 +00:00
Joseph T. Lyons
72f8fa6d1e Adjust Zed badge (#35290)
- Inline badges
- Set label background fill color to black
- Uppercase Zed text
- Remove gray padding

Release Notes:

- N/A
2025-07-29 14:24:10 -04:00
Joseph T. Lyons
902c17ac1a Add Zed badge to README.md (#35287)
Release Notes:

- N/A
2025-07-29 14:15:17 -04:00
Ben Kunkle
efa3cc13ef keymap_ui: Test keystroke input (#35286)
Closes #ISSUE

Separate out the keystroke input into it's own component and add a bunch
of tests for it's core keystroke+modifier event handling logic

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-29 14:10:51 -04:00
Michael Sloan
65250fe08d cloud provider: Use CompletionEvent type from zed_llm_client (#35285)
Release Notes:

- N/A
2025-07-29 17:28:18 +00:00
Marshall Bowers
77dc65d826 collab: Attach User-Agent to handle connection span (#35282)
This PR makes it so we attach the value from the `User-Agent` header to
the `handle connection` span.

We'll start sending this header in
https://github.com/zed-industries/zed/pull/35280.

Release Notes:

- N/A
2025-07-29 17:06:27 +00:00
Marshall Bowers
f9224b1d74 client: Send User-Agent header on WebSocket connection requests (#35280)
This PR makes it so we send the `User-Agent` header on the WebSocket
connection requests when connecting to Collab.

We use the user agent set on the parent HTTP client.

Release Notes:

- N/A
2025-07-29 16:53:56 +00:00
localcc
aa3437e98f Allow installing from an administrator user (#35202)
Release Notes:

- N/A
2025-07-29 18:03:57 +02:00
Finn Evers
397b5f9301 Ensure context servers are spawned in the workspace directory (#35271)
This fixes an issue where we were not setting the context server working
directory at all.

Release Notes:

- Context servers will now be spawned in the currently active project
root.

---------

Co-authored-by: Danilo Leal <danilo@zed.dev>
2025-07-29 18:03:43 +02:00
localcc
d43f464174 Fix nightly icon (#35204)
Release Notes:

- N/A
2025-07-29 18:01:07 +02:00
localcc
511fdaed43 Allow searching Windows paths with forward slash (#35198)
Release Notes:

- Searching windows paths is now possible with a forward slash
2025-07-29 17:58:28 +02:00
Marshall Bowers
a8bdf30259 client: Fix typo in the error message (#35275)
This PR fixes a typo in the error message for when we fail to parse the
Collab URL.

Release Notes:

- N/A
2025-07-29 15:45:49 +00:00
devjasperwang
2fced602b8 paths: Fix using relative path as custom_data_dir (#35256)
This PR fixes issue of incorrect LSP path args caused by using a
relative path when customizing data directory.

command:
```bash
.\target\debug\zed.exe --user-data-dir=.\target\data
```

before:
```log
2025-07-29T14:17:18+08:00 INFO  [lsp] starting language server process. binary path: "F:\\nvm\\nodejs\\node.exe", working directory: "F:\\zed\\target\\data\\config", args: [".\\target\\data\\languages\\json-language-server\\node_modules/vscode-langservers-extracted/bin/vscode-json-language-server", "--stdio"]
2025-07-29T14:17:18+08:00 INFO  [project::prettier_store] Installing default prettier and plugins: [("prettier", "3.6.2")]
2025-07-29T14:17:18+08:00 ERROR [lsp] cannot read LSP message headers
2025-07-29T14:17:18+08:00 ERROR [lsp] Shutdown request failure, server json-language-server (id 1): server shut down

2025-07-29T14:17:43+08:00 ERROR [project] Invalid file path provided to LSP request: ".\\target\\data\\config\\settings.json"
Thread "main" panicked with "called `Result::unwrap()` on an `Err` value: ()" at crates\project\src\lsp_store.rs:7203:54
cfd5b8ff10/src/crates\project\src\lsp_store.rs#L7203 (may not be uploaded, line may be incorrect if files modified)
```

after:
```log
2025-07-29T14:24:20+08:00 INFO  [lsp] starting language server process. binary path: "F:\\nvm\\nodejs\\node.exe", working directory: "F:\\zed\\target\\data\\config", args: ["F:\\zed\\target\\data\\languages\\json-language-server\\node_modules/vscode-langservers-extracted/bin/vscode-json-language-server", "--stdio"]
```

Release Notes:

- N/A
2025-07-29 15:31:54 +00:00
Peter Tripp
3fc84f8a62 Comment on source of ctrl-m in keymaps (#35273)
Closes https://github.com/zed-industries/zed/issues/23896

Release Notes:

- N/A
2025-07-29 15:29:12 +00:00
Kirill Bulatov
5a218d8323 Add more data to see which extension got leaked (#35272)
Part of https://github.com/zed-industries/zed/issues/35185

Release Notes:

- N/A
2025-07-29 15:24:52 +00:00
Agus Zubiaga
9353ba7887 Fix remaining agent server integration tests (#35222)
Release Notes:

- N/A
2025-07-29 12:40:59 +00:00
Finn Evers
8f952f1b58 gpui: Ensure first tab index is selected on first focus (#35247)
This fixes an issue with tab indices where we would actually focus the
second focus handle on first focus instead of the first one. The test
was updated accordingly.

Release Notes:

- N/A

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
2025-07-29 10:30:38 +00:00
Piotr Osiewicz
6c5791532e lsp: Remove Attach enum, default to Shared behaviour (#35248)
This should be a no-op PR, behavior-wise.

Release Notes:

- N/A
2025-07-29 10:07:36 +00:00
Kirill Bulatov
691b3ca238 Cache LSP code lens requests (#35207) 2025-07-29 09:51:58 +03:00
Cole Miller
cfd5b8ff10 python: Uplift basedpyright support into core (#35250)
This PR adds a built-in adapter for the basedpyright language server.

For now, it's behind the `basedpyright` feature flag, and needs to be
requested explicitly like this for staff:

```
  "languages": {
    "Python": {
      "language_servers": ["basedpyright", "!pylsp", "!pyright"]
    }
  }
```

(After uninstalling the basedpyright extension.)

Release Notes:

- N/A
2025-07-29 03:19:31 +00:00
Piotr Osiewicz
e5269212ad lsp/python: Temporarily report just a singular workspace folder instead of all of the roots (#35243)
Temporarily fixes #29133

Co-authored-by: Cole <cole@zed.dev>

Release Notes:

- python: Zed now reports a slightly different set of workspace folders
for Python projects to work around quirks in handling of multi-lsp
projects with virtual environment. This behavior will be revisited in a
near future.

Co-authored-by: Cole <cole@zed.dev>
2025-07-29 00:10:32 +00:00
Bedis Nbiba
d2ef287791 Add runnable support for Deno.test (#34593)
example of detected code:
```ts
Deno.test("t", () => {
  console.log("Hello, World!");
});

Deno.test(function azaz() {
  console.log("Hello, World!");
});
```

I can't build zed locally so I didn't test this, but I think the code is
straightforward enough, hopefully someone else can verify it

Closes #ISSUE

Release Notes:

- N/A
2025-07-29 01:45:41 +02:00
Tom Monaghan
109eddafd0 docs: Fix link in configuration documentation (#35249)
# Summary

The link "under the configuration page" [on this
page](https://zed.dev/docs/configuring-zed#agent) is broken. It should
be linking to [this page](https://zed.dev/docs/ai/configuration).

## Approach

I noted that all other links in this document begin with "./" where the
ai configuration link does not, I also noticed [this
PR](https://github.com/zed-industries/zed/pull/31119) fixing a link with
the same approach. I don't fully understand why this is the fix.

## Previous Approaches

I have tried writing the following redirect in `docs/book.toml`:
`"/ai/configuration.html" = "/docs/ai/configuration.html"`. However this
broke the `mdbook` build with the below error.

```
2025-07-29 08:49:36 [ERROR] (mdbook::utils): 	Caused By: Not redirecting "/Users/tmonaghan/dev/zed/docs/book/ai/configuration.html" to "/docs/ai/configuration.html" because it already exists. Are you sure it needs to be redirected?
```

Release Notes:

- N/A
2025-07-28 22:59:46 +00:00
Kirill Bulatov
798aa50df8 Fix tasks leaked despite workspace window close (#35246)
Closes https://github.com/zed-industries/zed/issues/34932

Release Notes:

- Fixed tasks leaked despite workspace window close
2025-07-28 22:37:48 +00:00
Julia Ryan
11c7b498b3 Fix panic feature flag detection (#35245)
The flag was being checked before feature flags were resolved.

Release Notes:

- N/A
2025-07-28 22:18:20 +00:00
Finn Evers
ca34ead6d9 onboarding: Add proper icon for action (#35241)
This change updates one icon within the onboarding flow to the indended
icon for that entry.

Release Notes:

- N/A
2025-07-28 22:02:12 +00:00
Finn Evers
158f65fd1e gpui: Ensure tab index handles are properly reused across frames (#35235)
This fixes an issue where focus handles with a tab index would get lost
between rendered frames because the focus handles were not reused for
the following paint cycle.

Release Notes:

- N/A
2025-07-28 23:33:20 +02:00
Ben Kunkle
fa6b1a0114 keymap_ui: Fix bug introduced in #35208 (#35237)
Closes #ISSUE

Fixes a bug that was cherry picked onto stable and preview branches
introduced in #35208 whereby modifier keys would show up and not be
removable when editing a keybind

Release Notes:

- (preview only) Keymap Editor: Fixed an issue introduced in v0.197.2
whereby modifier keys would show up and not be removable while recording
keystrokes in the keybind edit modal
2025-07-28 17:27:10 -04:00
Danilo Leal
f3dc842ce6 keymap editor: Make table denser (#35236)
Hopefully, this will make it a bit easier to parse as a whole.

Release Notes:

- Made the keymap editor denser, improving how easy you can parse it at
a glance.
2025-07-28 21:25:48 +00:00
Finn Evers
7ccf8c2f8c onboarding: Continue work on new flow (#35233)
This PR continues the work on the new and revamped onboarding flow.


Release Notes:

- N/A
2025-07-28 23:10:28 +02:00
Peter Tripp
8207621a4a Improve JetBrains keymap for dock toggling (#35234)
Follow-up to: 
- https://github.com/zed-industries/zed/pull/34641
- https://github.com/zed-industries/zed/pull/35230

This improves Zed's behavior with the Jetbrains keymap for toggling
specific docks/sidebars with cmd-0 thru cmd-9 (macos) and alt-0 thru
alt-9 (linux). Added in
https://github.com/zed-industries/zed/pull/34641. Additionally, this
also maps `ctrl-b` / `ctrl-alt-`b to their JetBrains equivalents
(`editor::GoToDefinition` and `editor::GoToDefinitionSplit`) instead of
the default vscode-compatable behavior (toggle left / right dock). This
is because we those specific toggles and a default Hide keyboard
shortcut (`shift-escape`) added in
https://github.com/zed-industries/zed/pull/35230.

Thanks to @thomaseizinger for raising this in:
-
https://github.com/zed-industries/zed/discussions/34643#discussioncomment-13856746

Release Notes:

- Improve support keyboard-based dock show/hide in Jetbrains keymap.
2025-07-28 21:00:01 +00:00
Marshall Bowers
ab90ed41da collab: Remove POST /billing/subscriptions/sync endpoint (#35232)
This PR removes the `POST /billing/subscriptions/sync` endpoint, as it
has been moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-28 20:22:54 +00:00
Peter Tripp
994d400ab8 Map shift-escape in the Jetbrains keymaps (#35230)
Release Notes:

- Added support for closing docks (sidebars) with `shift-escape` in the
Jetbrains keymaps.
2025-07-28 19:59:01 +00:00
Smit Barmase
cf13a76618 editor: Prioritize fuzzy score over sort positions in code completion sort (#35229)
We already prioritize matches that come after separators like `_`: 


cef7d53607/crates/fuzzy/src/matcher.rs (L274)

and deprioritize non-consecutive matches using distance penalty:


cef7d53607/crates/fuzzy/src/matcher.rs (L281)

In completion sort, letting fuzzy score be the primary sort factor and
sort positions be secondary yields better results upon testing. We still
need sort positions because of this kind of test case:


cef7d53607/crates/editor/src/code_completion_tests.rs (L195-L217)

Before/After:

<img height="250" alt="image"
src="https://github.com/user-attachments/assets/38495576-add6-4435-93f0-891f48ec9263"
/>
<img height="250" alt="image"
src="https://github.com/user-attachments/assets/0c73b835-0e23-4e30-a3ff-28bb56294239"
/>


Release Notes:

- N/A
2025-07-29 01:15:29 +05:30
Richard Feldman
b64977f6f4 Use zed settings to detect .zed folders (#35224)
Behind-the-scenes enhancement of
https://github.com/zed-industries/zed/pull/35221

Release Notes:

- N/A
2025-07-28 19:38:20 +00:00
Finn Evers
c3920b806b editor: Ensure code actions menu doesn't grow beyond its max size (#35211)
This resolves the same issue as fixed by
d295409f0f
for the code actions menu.

No release notes since this only occurs on Nightly.

Release Notes:

- N/A
2025-07-28 21:07:58 +02:00
Peter Tripp
055a9f21a0 Fix keybinds and 'No default binding' shown in docs (#35227)
Previously:
<img width="724" height="234" alt="Screenshot 2025-07-28 at 14 49 50"
src="https://github.com/user-attachments/assets/b5203316-221d-4af5-8190-124f9b2b4cdf"
/>


Release Notes:

- N/A
2025-07-28 14:58:43 -04:00
Richard Feldman
a55bd49c8d Allow edit tool to access files outside project (with confirmation) (#35221)
Now the edit tool can access files outside the current project (just
like the terminal tool can), but it's behind a prompt (unlike other edit
tool actions).

Release Notes:

- The edit tool can now access files outside the current project, but
only if the user grants it permission to.
2025-07-28 14:01:34 -04:00
Julia Ryan
a57e4dc8a8 Upload debug info to sentry.io in nightly builds (#35089)
This is a preparatory change which will allow us to use sentry for crash
reporting once we start uploading minidumps.

Release Notes:

- N/A
2025-07-28 17:42:36 +00:00
Ben Kunkle
eef15abbe4 keymap_ui: Additional keystroke input polish (#35208)
Closes #ISSUE

Fixed various issues and improved UX around the keystroke input
primarily when used for keystroke search.

Release Notes:

- Keymap Editor: FIxed an issue where the modifiers used to activate
keystroke search would appear in the keystroke search
- Keymap Editor: Made it possible to search for repeat modifiers, such
as a binding with `cmd-shift cmd`
- Keymap Editor: Made keystroke search matches match based on ordered
(not necessarily contiguous) runs. For example, searching for `cmd
shift-j` will match `cmd-k cmd-shift-j alt-q` and `cmd-i g shift-j` but
not `alt-k shift-j` or `cmd-k alt-j`
- Keymap Editor: Fixed the clear keystrokes binding (`delete` by
default) not working in the keystroke input
2025-07-28 13:15:21 -04:00
Danilo Leal
5aa8425449 Update Nightly app icon (#35215)
Follow-up to https://github.com/zed-industries/zed/pull/35119.

Release Notes:

- N/A
2025-07-28 17:01:07 +00:00
Todd L Smith
05d3473df8 Fix Nushell environment variables (#35166)
- Fixes environment variable ingestion for Nushell.

Closes #35056

Release Notes:

- N/A
2025-07-28 13:00:41 -04:00
Cole Miller
3dc36dc7c3 git: Touch up amend UX (#35114)
Follow-up to #26114

- Ensure that the previous commit message is filled in when toggling on
amend mode from the context menu
- Fix keybinding flicker in context menu

Release Notes:

- N/A
2025-07-28 12:21:07 -04:00
Agus Zubiaga
fd68265efd Fix integration tests for claude (#35212)
Release Notes:

- N/A
2025-07-28 16:18:01 +00:00
devjasperwang
5e2da042ef debugger: Fix the terminal popping up when the Rust debugger starts on Windows (#35125)
This PR fixs a terminal popping up when debugging Rust on Windows.


Release Notes:

- N/A

---------

Co-authored-by: 张小白 <364772080@qq.com>
2025-07-28 22:44:22 +08:00
Agus Zubiaga
c2fc70eef7 ACP over MCP server impl (#35196)
Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-07-28 14:14:10 +00:00
Danilo Leal
b02ae771cd ai onboarding: Add first-open upsell card (#35199)
Release Notes:

- N/A
2025-07-28 11:00:59 -03:00
张小白
3a1b1847c1 Fix debugger on Windows (#35180)
Closes #33429

Release Notes:

- N/A
2025-07-28 22:00:26 +08:00
Piotr Osiewicz
4aae7aed93 debugger: Fix not being able to stop a Go debug session when no breakpoints were ever hit (#35190)
Fixes #35030

Release Notes:

- debugger: Fixed a bug where a Go debug session could not be stopped if
no breakpoint was ever hit.
2025-07-28 13:14:36 +00:00
Danilo Leal
cef7d53607 collab: Refine call buttons design (#35007)
Making icons consistent, adjusting spacing, and moving the "Leave Call"
button to be the very last, which makes more sense to me than the
"Share" button being the last. Sharing your project is still part of the
call, so in the left edge of the button strip is where, conceptually,
the option to end the call should be, I think!

Release Notes:

- N/A
2025-07-28 10:01:31 -03:00
Maksim Bondarenkov
3ad0546b0d Re-add TestScreenCaptureStream implementation for mock_client to fix FreeBSD and MinGW builds (#35191)
The implementation was removed in #31506. re-added it to match the
current implementation from `gpui::test`

Closes #35189

Release Notes:

- N/A
2025-07-28 14:02:48 +02:00
Piotr Osiewicz
e38f5759c4 ci: Use cargo update --locked instead of --frozen (#35192)
This fixes false positives when e.g. bumping git deps

Release Notes:

- N/A
2025-07-28 12:01:00 +00:00
Robert Fratto
2566acc2e7 go: Support benchmarks named "Benchmark" (#35167)
The regular expression for benchmarks was enforcing using a suffix
(e.g., `BenchmarkFoo`), but `Benchmark` is a valid benchmark name, just
as `Test` is a valid test name, and `Fuzz` is a valid fuzz test name.

Release Notes:

- Add support for running Go benchmarks named "Benchmark"
2025-07-28 10:40:46 +00:00
Smit Barmase
45b3af713e doc: Add docs for Forcing X11 scale factor (#35181)
Associated PR: https://github.com/zed-industries/zed/pull/34265
Recent Discussion:
https://github.com/zed-industries/zed/issues/33987#issuecomment-3125936302

Release Notes:

- N/A
2025-07-28 14:31:27 +05:30
marius851000
ee9b60e60c gpui: Fix inset being used in SSD on Wayland (#35151)
Closes #31330

Second parts of https://github.com/zed-industries/zed/pull/31335

While the initial fix set the inset during drawing, that was after the
window was resized, resulting in needing to manually resize the window
for the change to properly take effect.

I updated the code to not make the Wayland renderer rely on
`client_inset` being updated by the API user to match with the
decoration mode (given it is supposed to only be used when using CSD).

I might later try to generalize that, and eventually make the
client_inset only defined on window creation (instead of inside
`client_side_decorations`, that would need testing on X) (and maybe also
allow configuration for shadow, but it’s not something I need).

Release Notes:

- Fixed switching from client side decoration to server side decoration
on Wayland
2025-07-28 11:36:00 +03:00
lowlandghost
2a0aad0aaa Docs: Fix invalid JSON syntax in Visual Customizations - Editor Scrollbar and Minimap (#35159)
I was on the [Visual Customiztions - Editor
Scrollbar](https://zed.dev/docs/visual-customization#editor-scrollbar)
section of the docs, and copy and pasted the code block into my personal
Zed settings and saw there was a syntax error.

This is a PR to add a missing comma and fix the syntax error in the
docs.

First time contributing, please let me know if I missed any
steps/important info.

Release Notes:

- N/A
2025-07-27 20:56:55 +03:00
Danilo Leal
e7b5d93b7c docs: Add more improvements to the AI docs (#35160)
Follow-up to https://github.com/zed-industries/zed/pull/35133 with some
more findings upon re-reviewing it again.

Release Notes:

- N/A
2025-07-27 14:06:18 -03:00
Ben Brandt
c995d45bd9 agent_servers: Include result text in Claude error messages (#35156)
This will better surfaces issues that are classified as "success" but
actually have a more meaningful error message attached.

Release Notes:

- N/A
2025-07-27 13:58:02 +00:00
Ben Brandt
a5b7cfd128 agent_servers: Use built-in interrupt handling for Claude sessions (#35154)
We no longer have to stop and restart the entire process. 
I left in the Start/Resume mode handling since we will likely need to
handle restarting Claude in other situations.

Release Notes:

- N/A
2025-07-27 13:50:04 +00:00
Marshall Bowers
89e88c245e extension_host: Add npm:install capability (#35144)
This PR adds a new `npm:install` capability for installing npm packges
in extensions.

Currently all npm packages are allowed.

Release Notes:

- N/A
2025-07-26 22:40:02 +00:00
Marshall Bowers
2a0170dc3c extension: Reorganize capabilities (#35143)
This PR reorganizes the capabilities within the `extension` crate to
make it easier to add more.

Release Notes:

- N/A
2025-07-26 22:05:22 +00:00
Marshall Bowers
6a9a539b10 extension_host: Add capability for downloading files (#35141)
This PR adds a new capability for downloading files in extensions.

Currently all file downloads are allowed.

Release Notes:

- N/A
2025-07-26 21:33:16 +00:00
Marshall Bowers
d7b403e981 extension_host: Refactor capability checks (#35139)
This PR refactors the extension capability checks to be centralized in
the `CapabilityGranter`.

Release Notes:

- N/A
2025-07-26 20:53:19 +00:00
Danilo Leal
290f84a9e1 docs: Restructure and improve AI configuration docs (#35133)
Adding a number of settings that weren't documented, restructuring
things a bit to separate what is model-related settings from agent panel
usage-related settings, adding the recently introduced `disable_ai` key,
and more.

Release Notes:

- Improved docs around configuring and using AI in Zed
2025-07-26 16:06:12 +00:00
Finn Evers
08402e25e0 ui: Fix scrollbar mouse down handler (#35131)
Follow-up to https://github.com/zed-industries/zed/pull/35121 - in the
process of adding the check for the left mouse button to the guard
(which was practically already there before, just not in the mouse-down
listener), I accidentally removed the negation for the bounds check.
This PR fixes this.

Release Notes:

- N/A
2025-07-26 15:45:13 +00:00
Finn Evers
e911364664 ui: Clean up scrollbar component (#35121)
This PR does some minor cleanup to the scrollbar component. Namely, it
removes some clones, reduces the amount of unnecessary notifies and
ensures the scrollbar hover state is more accurately updated.

Release Notes:

- N/A
2025-07-26 00:08:20 +00:00
Danilo Leal
4854f83e8c agent: Fix settings view switch field about permissions for running commands (#35120)
Follow-up to https://github.com/zed-industries/zed/pull/34866, where I
mistakenly didn't update the copy and id for this item.

Closes https://github.com/zed-industries/zed/issues/34850 — that's
because the edit tool runs by default, but the terminal tool does not. I
updated the original copy to reflect this, which should be enough to
close the issue, given that in terms of behavior, it is working
correctly.

Release Notes:

- agent: Fixed duplicated settings item in the agent panel as well as
improve copy for the setting to allow running commands without
permission.
2025-07-26 00:00:44 +00:00
Danilo Leal
6a79c4e50a ui: Add ToggleButtonGroup component (#35118)
<img width="600" height="704" alt="CleanShot 2025-07-25 at 8  03 04@2x"
src="https://github.com/user-attachments/assets/3d2b29ba-e0fd-4231-bb80-746903d61481"
/>

Release Notes:

- N/A

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
2025-07-25 20:32:53 -03:00
Danilo Leal
aea309160b Update the new Nightly app icon (#35119)
Follow-up to https://github.com/zed-industries/zed/pull/35104, with some
tiny design updates.

Release Notes:

- N/A
2025-07-25 23:29:51 +00:00
Smit Barmase
43d0aae617 languages: Fix Bash indentation issues with multi-cursors, newlines, and keyword outdenting (#35116)
Closes #34390

This PR fixes several Bash indentation issues:

- Adding indentation or comment using multi cursors no longer breaks
relative indentation
- Adding newline now places the cursor at the correct indent
- Typing a valid keyword triggers context-aware auto outdent

It also adds tests for all of them.

Release Notes:

- Fixed various issues with handling indentation in Bash.
2025-07-26 04:58:10 +05:30
Alvaro Parker
07252c3309 git: Enable git stash in git panel (#32821)
Related discussion #31484

Release Notes:

- Added a menu entry on the git panel to git stash and git pop stash. 

Preview: 


![Screenshot-2025-06-17_08:26:36](https://github.com/user-attachments/assets/d3699ba4-511f-4c7b-a7cc-00a295d01f64)

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-07-25 23:15:54 +00:00
Max Brunsfeld
4d00d07df1 Render paths to a single fixed-size MSAA texture (#34992)
This is another attempt to solve the same problem as
https://github.com/zed-industries/zed/pull/29718, while avoiding the
regression on Intel GPUs.

###  Background

Currently, on main, all paths are first rendered to an intermediate
"atlas" texture, similar to what we use for rendering glyphs, but with
multi-sample antialiasing enabled. They are then drawn into our actual
frame buffer in a separate pass, via the "path sprite" shaders.

Notably, the intermediate texture acts as an "atlas" - the paths are
laid out in a non-overlapping way, so that each path could be copied to
an arbitrary position in the final scene. This non-overlapping approach
makes a lot sense for Glyphs (which are frequently re-used in multiple
places within a frame, and even across frames), but paths do not have
these properties.
* we clear the atlas every frame
* we rasterize each path separately. there is no deduping.

The problem with our current approach is that the path atlas textures
can end up using lots of VRAM if the scene contains many paths. This is
more of a problem in other apps that use GPUI than it is in Zed, but I
do think it's an issue for Zed as well. On Windows, I have hit some
crashes related to GPU memory.

In https://github.com/zed-industries/zed/pull/29718, @sunli829
simplified path rendering to just draw directly to the frame buffer, and
enabled msaa for the whole frame buffer. But apparently this doesn't
work well on Intel GPUs because MSAA is slow on those GPUs. So we
reverted that PR.

### Solution

With this PR, we rasterize paths to an intermediate texture with MSAA.
But rather than treating this intermediate texture like an *atlas*
(growing it in order to allocate non-overlapping rectangles for every
path), we simply use a single fixed-size, color texture that is the same
size as thew viewport. In this texture, we rasterize the paths in their
final screen position, allowing them to overlap. Then we simply blit
them from the resolved texture to the frame buffer.

### To do

* [x] Implement for Metal
* [x] Implement for Blade
* [x] Fix content masking for paths
* [x] Fix rendering of partially transparent paths
* [x] Verify that this performs well on Intel GPUs (help @notpeter 🙏 )
* [ ] Profile and optimize

Release Notes:

- N/A

---------

Co-authored-by: Junkui Zhang <364772080@qq.com>
2025-07-25 14:39:24 -07:00
Cole Miller
bf8e4272bc Add a CI check that Cargo.lock is up to date (#35111)
Should catch cases like #33667 where a dependency was bumped in
Cargo.toml without a corresponding lockfile update. (This can happen
when committing from outside of Zed or if rust-analyzer isn't running.)
Passing `--frozen --workspace` should ensure this doesn't fail just
because there's a newer patch version of some third-party dependency,
and prevent it from taking long.

We only need to run this on macOS because Cargo.lock is
platform-independent.

Release Notes:

- N/A
2025-07-25 15:37:58 -04:00
Marshall Bowers
f787f7d291 Update Cargo.lock (#35106)
This PR updates the `Cargo.lock` file, as it seems like it wasn't fully
updated in https://github.com/zed-industries/zed/pull/35103.

Release Notes:

- N/A
2025-07-25 18:20:18 +00:00
Danilo Leal
0e7eea0d10 Add new Nightly app icons (#35104)
Release Notes:

- N/A
2025-07-25 17:39:33 +00:00
Cole Miller
ff67f18e0d Bump livekit dep to try to fix flaky builds (#35103)
Let's see if https://github.com/zed-industries/livekit-rust-sdks/pull/6
helps.

Release Notes:

- N/A

---------

Signed-off-by: Cole Miller <cole@zed.dev>
2025-07-25 17:36:58 +00:00
Anthony Eid
4abe14f94a keymap ui: Resize columns on double click improvement (#35095)
This PR improves the behavior of resetting a column's size by
double-clicking on its column handle. We now shrink/grow to the side
that has more leftover/additional space.

We also improved the below

1. dragging was a couple of pixels off to the left because we didn't
take the column handle’s width into account.
2. Column dragging now has memory and will shift things to their exact
position when reversing a drag before letting the drag handle go.
3. Improved our test infrastructure.
4. Double clicking on a column's header resizes the column

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-25 13:19:05 -04:00
Kainoa Kanter
cd50958727 Add icon for SurrealQL files (#34855)
Release Notes:

- Added icon for SurrealQL (`.surql`) files

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-07-25 17:17:16 +00:00
Umesh Yadav
2f812c339c agent_ui: Fix delay when loading keybindings in the Agent panel settings (#34954)
Fixes a annoying lag in agent settings panel where the keybindings would
show up after a lag. Close to 1-2 secs. It was a simple fix previously
we were not passing the focus handler to context menu which made the
keybindings lookup slower compared to other parts like git panel and
title bar profile dropdown.

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/1f9c1861-d089-41d3-8a89-4334d7b2f43a"
controls preload></video> | <video
src="https://github.com/user-attachments/assets/fa33d58d-a1ae-48cf-bff7-8e0ee07ed4e1"
controls preload></video> |





Release Notes:

- Fix delay when loading keybindings in the Agent panel settings
2025-07-25 13:55:38 -03:00
Haojian Wu
993d5753d5 docs: Add a missing "," in the C/C++ debugger configuration (#35098)
Release Notes:

- N/A
2025-07-25 16:14:44 +00:00
Piotr Osiewicz
abb3ed1ed1 git: Fix commit modal contents being searchable (#35099)
Fixes #35093

Release Notes:

- Fixed Git commit editor being searchable.
2025-07-25 15:48:09 +00:00
Piotr Osiewicz
985350f9e8 terminal: Support ~ in cwd field of task definitions (#35097)
Closes #35022

Release Notes:

- Fixed `~` not being expanded correctly in `cwd` field of task
definitions.
2025-07-25 15:39:50 +00:00
Justin Su
0e9d955e9b Hide Copilot commands when AI functionality is disabled (#35055)
Follow-up of https://github.com/zed-industries/zed/pull/34896

Related to https://github.com/zed-industries/zed/issues/31346

cc @rtfeldman

Release Notes:

- Hide copilot commands when AI functionality is disabled
2025-07-25 10:53:57 -04:00
etimvr
5de544eb4b Fix unnecessary Ollama model loading (#35032)
Closes https://github.com/zed-industries/zed/issues/35031

Similar solution as in https://github.com/zed-industries/zed/pull/30589

Release Notes:

- Fix unnecessary ollama model loading
2025-07-25 16:58:05 +03:00
Umesh Yadav
f21ba9e2c6 lmstudio: Propagate actual error message from server (#34538)
Discovered in this issue: #34513

Previously, we were propagating deserialization errors to users when
using LMStudio, instead of the actual error message sent from the
LMStudio server. This change will help users understand why their
request failed while streaming responses.

Release Notes:

- lmsudio: Display specific backend error messaging on failure rather
than generic ones

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-25 09:36:43 -04:00
Julia Ryan
a2408f353b Don't separately rebuild crates for the build platform (#35084)
macos:
- `cargo build`: 1838 -> 1400
- `cargo build -r`1891 -> 1400

linux:
- `cargo build`: 1893 -> 1455
- `cargo build -r`: 1893 -> 1455

windows: 
- `cargo build`: 1830 -> 1392
- `cargo build -r`: 1880 -> 1392

We definitely want this change for debug builds, for release builds it's
_possible_ that it pessimizes the critical path, but we'll see if it
impacts CI times before merging.

Release Notes:

- N/A

---------

Co-authored-by: Rahul Butani <rrbutani@users.noreply.github.com>
2025-07-25 13:12:55 +00:00
Julia Ryan
9071341a1d Add TestCrash action (#35088)
This triggers a crash that avoids our panic-handler, which is useful for
testing that our crash reporting works even when you don't get a panic.

Release Notes:

- N/A
2025-07-25 05:40:26 -07:00
Piotr Osiewicz
57b463fd0d typescript: Fix handling of jest/vitest tests with regex characters in name (#35090)
Closes #35065

Release Notes:

- JavaScript/TypeScript tasks: Fixed handling of Vitest/Jest tests with
regex-specific characters in their name.
2025-07-25 12:23:52 +00:00
Piotr Osiewicz
631f9a1b31 worktree: Improve performance with large # of repositories (#35052)
In this PR we've reworked how git status updates are processed. Most
notable change is moving the processing into a background thread (and
splitting it across multiple background workers). We believe it is safe
to do so, as worktree events are not deterministic (fs updates are not
guaranteed to come in any order etc), so I've figured that git store
should not be overly order-reliant anyways.

Note that this PR does not solve perf issues wholesale - other parts of
the system are still slow to process stuff (which I plan to nuke soon).

Related to #34302

Release Notes:

- Improved Zed's performance in projects with large # of repositories

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-07-25 13:18:19 +02:00
Agus Zubiaga
af0c909924 McpServerTool output schema (#35069)
Add an `Output` associated type to `McpServerTool`, so that we can
include its schema in `tools/list`.

Release Notes:

- N/A
2025-07-25 02:57:18 +00:00
Agus Zubiaga
15c9da4ea4 Add ability to register tools in McpServer (#35068)
Makes it easier to add tools to a server by implementing a trait

Release Notes:

- N/A
2025-07-25 02:19:20 +00:00
morgankrey
b446d66be7 Telemetry docs cleanup (#35060)
Release Notes:

- N/A
2025-07-24 19:25:39 -06:00
morgankrey
a0f2019b6f Add sales tax to docs (#35059)
Release Notes:

- N/A
2025-07-24 19:18:40 -06:00
Smit Barmase
6e4f747041 project_panel: Fix autoscroll to treat entries behind sticky items as out of viewport (#35067)
Closes #34831

Autoscroll centers items only if they’re out of viewport. Before this
PR, entry behind sticky items was not considered out of viewport, and
hence actions like `reveal in project panel` or focusing buffer would
not autoscroll that entry into the view in that case.

This PR fixes that by using recently added `scroll_to_item_with_offset`
in https://github.com/zed-industries/zed/pull/35064.

Release Notes:

- Fixed issue where `pane: reveal in project panel` action was not
working if the entry was behind sticky items.
2025-07-25 06:21:38 +05:30
Daniel Sauble
4ee52433ae Do not subtract gutter margin twice from the editor width (#34564)
Closes #33176

In auto-height layouts, horizontal autoscroll can occur right before
soft wrapping is triggered. This seems to be caused by gutter margin
being subtracted twice from the editor width.

If we subtract gutter width only once, the horizontal autoscroll
behavior goes away.

Before:


https://github.com/user-attachments/assets/03b6687e-3c0e-4b34-8e07-a228bcc6f798

After:


https://github.com/user-attachments/assets/84e54088-b5bd-4259-a193-d9fcf32cd3a7

Release Notes:

- Fixes issue with auto-height layouts where horizontal autoscroll is
triggered right before text wraps

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-07-25 00:00:59 +00:00
Smit Barmase
f78a112387 gpui: Add scroll_to_item_with_offset to UniformListScrollState (#35064)
Previously we had `ScrollStrategy::ToPosition(usize)` which lets you
define the offset where you want to scroll that item to. This is the
same as `ScrollStrategy::Top` but imagine some space reserved at the
top.

This PR removes `ScrollStrategy::ToPosition` in favor of
`scroll_to_item_with_offset` which is the method to do the same. The
reason to add this method is that now not just `ScrollStrategy::Top` but
`ScrollStrategy::Center` can also uses this offset to center the item in
the remaining unreserved space.

```rs
// Before
scroll_handle.scroll_to_item(index, ScrollStrategy::ToPosition(offset));

// After
scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, offset);

// New! Centers item skipping first x items
scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Center, offset);
```

This will be useful for follow up PR.

Release Notes:

- N/A
2025-07-25 05:13:09 +05:30
Zachary Hamm
66acc2698a git_hosting_providers: Support GitHub remote URLs that start with a slash (#34134)
Remote URLs like `git@github.com:/zed-industries/zed` are valid git
remotes, but currently Zed does not parse them correctly. The result is
invalid "permalink" generation, and presumably other bugs anywhere that
git remote urls are required for the current repository. This ensures
Zed will parse those URLs correctly.

Release Notes:

- Improved support for GitHub remote urls where the
username/organization is preceded by an extra `/` to match the behavior
of `git`, `gh` and `github.com`.

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-07-24 18:25:21 -04:00
Peter Tripp
707df51664 Fix environment loading with tcsh (#35054)
Closes https://github.com/zed-industries/zed/issues/34973

Fixes an issue where environment variables were not loaded when the
user's shell was tcsh and instead a file named `0` was dumped in the
current working directory with a copy of your environment variables as
json.

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

Release Notes:

- Fixed a regression with loading environment variables in nushell
2025-07-24 15:39:13 -04:00
Mikayla Maki
13df1dd5ff Increase the number of parallel request handlers per connection (#35046)
Discussed this with @ConradIrwin. The problem we're having with collab
is that a bunch of LSP requests take a really long time to resolve,
particularly `RefreshCodeLens`. But Those requests are sharing a limited
amount of concurrency that we've allocated for all message traffic on
one connection. That said, there's not _that_ many concurrent requests
made at any one time. The burst traffic seems to top out in the low
hundreds for these requests. So let's just expand the amount of space in
the queue to accommodate these long-running requests while we work on
upgrading our cloud infrastructure.

Release Notes:

- N/A

Co-authored-by: finn <finn@zed.dev>
2025-07-24 11:44:26 -07:00
Peter Tripp
1f7ff956bc Fix environment loading with nushell (#35002)
Closes https://github.com/zed-industries/zed/issues/34739

I believe this is a regression introduced here:
- https://github.com/zed-industries/zed/pull/33599

Release Notes:

- Fixed a regression with loading environment variables in nushell
2025-07-24 14:29:28 -04:00
Agus Zubiaga
2d0f10c48a Refactor to use new ACP crate (#35043)
This will prepare us for running the protocol over MCP

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-07-24 14:39:29 -03:00
Joseph T. Lyons
45ddf32a1d Fix variable name in project type reporting (#35045)
Release Notes:

- N/A
2025-07-24 17:08:02 +00:00
Kirill Bulatov
2658b2801e Shutdown language servers better (#35038)
Follow-up of https://github.com/zed-industries/zed/pull/33417

* adjust prettier mock LSP to handle `shutdown` and `exit` messages
* removed another `?.log_err()` backtrace from logs and improved the
logging info
* always handle the last parts of the shutdown logic even if the
shutdown response had failed

Release Notes:

- N/A
2025-07-24 15:24:53 +00:00
Richard Feldman
2a9355a3d2 Don't auto-retry in certain circumstances (#35037)
Someone encountered this in production, which should not happen:

<img width="1266" height="623" alt="Screenshot 2025-07-24 at 10 38
40 AM"
src="https://github.com/user-attachments/assets/40f3f977-5110-4808-a456-7e708d953b3b"
/>

This moves certain errors into the category of "never retry" and reduces
the number of retries for some others. Also it adds some diagnostic
logging for retry policy.

It's not a complete fix for the above, because the underlying issues is
that the server is sending a HTTP 403 response and although we were
already treating 403s as "do not retry" it was deciding to retry with 2
attempts anyway. So further debugging is needed to figure out why it
wasn't going down the 403 branch by the time the request got here.

Release Notes:

- N/A
2025-07-24 11:11:26 -04:00
Smit Barmase
fa788a39a4 project_panel: Reuse index_for_entry in index_for_selection (#35034)
Just refactor I came across while working on another issue.
`index_for_entry` and `index_for_selection` have the exact same logic,
here we can simply reuse `index_for_entry` for `index_for_selection`.

Release Notes:

- N/A
2025-07-24 20:07:38 +05:30
Pablo Ramón Guevara
7cdd808db2 helix: Fix replace in helix mode (#34789)
Closes https://github.com/zed-industries/zed/issues/33076

Release Notes:

- Fixed replace command on helix mode: now it actually replaces what was
selected and keeps the replaced text selected to better match helix
2025-07-24 08:29:58 -06:00
Danilo Leal
29332c1962 ai onboarding: Add overall fixes to the whole flow (#34996)
Closes https://github.com/zed-industries/zed/issues/34979

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
2025-07-24 11:26:15 -03:00
Peter Tripp
fab450e39d Fix invalid regular expressions highlighting all search fields (#35001)
Closes https://github.com/zed-industries/zed/issues/34969
Closes https://github.com/zed-industries/zed/issues/34970

Only highlight the search field on regex error (buffer search and
project search).
Clear errors when the buffer search hidden so stale errors aren't shown
on next search.

Before (all fields highlighted red): 
<img width="631" height="180" alt="Screenshot 2025-07-23 at 22 59 45"
src="https://github.com/user-attachments/assets/a91d3090-1bae-4718-a1f5-0a1237559ff2"
/>

After (only query field highlighted red): 
<img width="632" height="187" alt="Screenshot 2025-07-23 at 23 10 49"
src="https://github.com/user-attachments/assets/6ae72c84-9333-4cdb-907b-afb7a0a6e808"
/>

Release Notes:

- Improved highlighting of regex errors in search dialogs
2025-07-24 09:20:25 -04:00
Danilo Leal
4fb540d6d2 dock: Add divider between panels on the right side, too (#35003)
A while ago, we added a divider in the left side of the status bar
between the icon buttons that open panels vs. those that open something
else (tabs, popover menus, etc.). There isn't a super good reason why we
wouldn't do the same in the right side, even more so given that (at
least by default) we usually have more buttons on that side; buttons
that _don't_ open panels (regardless of being docked to the bottom or
right). So, this PR does this!

<img width="700" height="314" alt="CleanShot 2025-07-24 at 1  59 10@2x"
src="https://github.com/user-attachments/assets/5f8bd4bc-a983-4000-a8f9-05a36b9e4b81"
/>

Release Notes:

- N/A
2025-07-24 09:39:10 -03:00
Piotr Osiewicz
1e2b0fcab6 sum_tree: Remove Unit type (#35027)
This solves one ~TODO, as Unit type was a workaround for a lack of
ability to implement Summary for ().


Release Notes:

- N/A
2025-07-24 14:30:49 +02:00
Oleksiy Syvokon
0af690080b linux: Fix ctrl-0..9, ctrl-[, ctrl-^ (#35028)
There were two different underlying reasons for the issues with
ctrl-number and ctrl-punctuation:

1. Some keys in the ctrl-0..9 range send codes in the `\1b`..`\1f`
range. For example, `ctrl-2` sends keycode for `ctrl-[` (0x1b), but we
want to map it to `2`, not to `[`.

2. `ctrl-[` and four other ctrl-punctuation were incorrectly mapped,
since the expected conversion is by adding 0x40

Closes #35012

Release Notes:

- N/A
2025-07-24 12:22:57 +00:00
366 changed files with 21546 additions and 6287 deletions

View File

@@ -19,7 +19,7 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- name: Check for broken links
- name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress --exclude '^http' './docs/src/**/*'
@@ -30,3 +30,9 @@ runs:
run: |
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Check for broken links (in HTML)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress --exclude '^http' 'target/deploy/docs/'
fail: true

View File

@@ -269,6 +269,10 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
- name: Check that Cargo.lock is up to date
run: |
cargo update --locked --workspace
- name: cargo clippy
run: ./script/clippy
@@ -767,7 +771,8 @@ jobs:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-hosted, Windows, X64]
if: false && (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
needs: [windows_tests]
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}

View File

@@ -111,6 +111,11 @@ jobs:
echo "Publishing version: ${version} on release channel nightly"
echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Create macOS app bundle
run: script/bundle-mac
@@ -136,6 +141,11 @@ jobs:
- name: Install Linux dependencies
run: ./script/linux && ./script/install-mold 2.34.0
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
@@ -168,6 +178,11 @@ jobs:
- name: Install Linux dependencies
run: ./script/linux
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100
@@ -262,6 +277,11 @@ jobs:
Write-Host "Publishing version: $version on release channel nightly"
"nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL"
- name: Setup Sentry CLI
uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2
with:
token: ${{ SECRETS.SENTRY_AUTH_TOKEN }}
- name: Build Zed installer
working-directory: ${{ env.ZED_WORKSPACE }}
run: script/bundle-windows.ps1

521
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
[workspace]
resolver = "2"
members = [
"crates/activity_indicator",
"crates/acp_thread",
"crates/agent_ui",
"crates/activity_indicator",
"crates/agent",
"crates/agent_settings",
"crates/ai_onboarding",
"crates/agent_servers",
"crates/agent_settings",
"crates/agent_ui",
"crates/ai_onboarding",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -29,6 +29,9 @@ members = [
"crates/cli",
"crates/client",
"crates/clock",
"crates/cloud_api_client",
"crates/cloud_api_types",
"crates/cloud_llm_client",
"crates/collab",
"crates/collab_ui",
"crates/collections",
@@ -48,8 +51,8 @@ members = [
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/editor",
"crates/explorer_command_injector",
"crates/eval",
"crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
@@ -70,7 +73,6 @@ members = [
"crates/gpui",
"crates/gpui_macros",
"crates/gpui_tokio",
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
@@ -99,7 +101,6 @@ members = [
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/svg_preview",
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
@@ -140,6 +141,7 @@ members = [
"crates/semantic_version",
"crates/session",
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
@@ -152,6 +154,7 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/svg_preview",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -251,6 +254,9 @@ channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
client = { path = "crates/client" }
clock = { path = "crates/clock" }
cloud_api_client = { path = "crates/cloud_api_client" }
cloud_api_types = { path = "crates/cloud_api_types" }
cloud_llm_client = { path = "crates/cloud_llm_client" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
@@ -337,6 +343,7 @@ picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
settings_profile_selector = { path = "crates/settings_profile_selector" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@@ -413,7 +420,7 @@ zlog_settings = { path = "crates/zlog_settings" }
#
agentic-coding-protocol = "0.0.10"
agent-client-protocol = { path = "../agent-client-protocol" }
agent-client-protocol = "0.0.11"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -460,7 +467,7 @@ core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
@@ -645,7 +652,6 @@ which = "6.0.0"
windows-core = "0.61"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "= 0.8.6"
zstd = "0.11"
[workspace.dependencies.async-stripe]
@@ -672,14 +678,16 @@ features = [
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D_Fxc",
"Win32_Graphics_DirectComposition",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
@@ -720,6 +728,11 @@ workspace-hack = { path = "tooling/workspace-hack" }
split-debuginfo = "unpacked"
codegen-units = 16
# mirror configuration for crates compiled for the build platform
# (without this cargo will compile ~400 crates twice)
[profile.dev.build-override]
codegen-units = 16
[profile.dev.package]
taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 }

View File

@@ -1,5 +1,6 @@
# Zed
[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).

8
assets/badge/v0.json Normal file
View File

@@ -0,0 +1,8 @@
{
"label": "",
"message": "Zed",
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 96 96\"><rect width=\"96\" height=\"96\" fill=\"#000\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z\" fill=\"#fff\"/></svg>",
"logoWidth": 16,
"labelColor": "black",
"color": "white"
}

View File

@@ -1 +1,7 @@
<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-volume-off"><path d="M16 9a5 5 0 0 1 .95 2.293"/><path d="M19.364 5.636a9 9 0 0 1 1.889 9.96"/><path d="m2 2 20 20"/><path d="m7 7-.587.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298V11"/><path d="M9.828 4.172A.686.686 0 0 1 11 4.657v.686"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 6C11.003 6.44823 11.2208 6.97398 11.3001 7.52867" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.9094 3.75732C13.7621 4.6095 14.3383 5.69876 14.5629 6.88315C14.7875 8.06754 14.6502 9.29213 14.1688 10.3973" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.66675 2L13.6667 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.33333 4.66669L4.942 5.05802C4.85494 5.1456 4.75136 5.21504 4.63726 5.2623C4.52317 5.30957 4.40083 5.33372 4.27733 5.33335H2.66667C2.48986 5.33335 2.32029 5.40359 2.19526 5.52862C2.07024 5.65364 2 5.82321 2 6.00002V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3088 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2646 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8654V7.33335" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.21875 2.78136C7.28267 2.71719 7.36421 2.67345 7.45303 2.65568C7.54184 2.63791 7.63393 2.64691 7.71762 2.68154C7.80132 2.71618 7.87284 2.77488 7.92312 2.85022C7.97341 2.92555 8.0002 3.01412 8.00008 3.10469V3.56202" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 527 B

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +1,5 @@
<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-volume-2"><path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><path d="M16 9a5 5 0 0 1 0 6"/><path d="M19.364 18.364a9 9 0 0 0 0-12.728"/></svg>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 3.13467C7.99987 3.04181 7.97223 2.95107 7.92057 2.8739C7.86892 2.79674 7.79557 2.7366 7.70977 2.70108C7.62397 2.66557 7.52958 2.65626 7.43849 2.67434C7.34741 2.69242 7.26373 2.73707 7.198 2.80266L4.942 5.058C4.85494 5.14558 4.75136 5.21502 4.63726 5.26228C4.52317 5.30954 4.40083 5.33369 4.27733 5.33333H2.66667C2.48986 5.33333 2.32029 5.40357 2.19526 5.52859C2.07024 5.65362 2 5.82319 2 6V10C2 10.1768 2.07024 10.3464 2.19526 10.4714C2.32029 10.5964 2.48986 10.6667 2.66667 10.6667H4.27733C4.40083 10.6663 4.52317 10.6905 4.63726 10.7377C4.75136 10.785 4.85494 10.8544 4.942 10.942L7.19733 13.198C7.26307 13.2639 7.34687 13.3087 7.43813 13.3269C7.52939 13.3451 7.62399 13.3358 7.70995 13.3002C7.79591 13.2645 7.86936 13.2042 7.921 13.1268C7.97263 13.0494 8.00013 12.9584 8 12.8653V3.13467Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.6667 6C11.0995 6.57699 11.3334 7.27877 11.3334 8C11.3334 8.72123 11.0995 9.42301 10.6667 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.9094 12.2427C13.4666 11.6855 13.9085 11.0241 14.2101 10.2961C14.5116 9.56815 14.6668 8.78793 14.6668 7.99999C14.6668 7.21205 14.5116 6.43183 14.2101 5.70387C13.9085 4.97591 13.4666 4.31448 12.9094 3.75732" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 475 B

After

Width:  |  Height:  |  Size: 1.4 KiB

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-cloud-download-icon lucide-cloud-download"><path d="M12 13v8l-4-4"/><path d="m12 21 4-4"/><path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"/></svg>

After

Width:  |  Height:  |  Size: 372 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2716_663)">
<path d="M8.47552 2.45453C11.5167 2.45457 13.9814 4.94501 13.9814 8.01623C13.9814 11.0875 11.5167 13.578 8.47552 13.5781C5.43427 13.5781 2.96948 11.0875 2.96948 8.01623C2.9695 4.94498 5.43429 2.45453 8.47552 2.45453ZM10.8795 4.70348C10.7605 4.16887 10.1328 3.85468 9.53627 3.96342C8.97622 4.06552 7.62871 4.45681 7.62057 4.45916C9.29414 4.44469 9.57429 4.4726 9.69939 4.64751C9.77324 4.7508 9.66576 4.89248 9.21944 4.96538C8.73515 5.04447 7.73014 5.13958 7.72343 5.14022C6.75441 5.19776 6.07177 5.20168 5.86705 5.63512C5.73334 5.91827 6.00968 6.16857 6.13082 6.32527C6.64271 6.89455 7.38215 7.20158 7.85809 7.42767C8.03716 7.51274 8.56257 7.67345 8.56257 7.67345C7.01855 7.58853 5.90474 8.06267 5.2514 8.60855C4.51246 9.29204 4.83937 10.1067 6.35327 10.6084C7.24742 10.9047 7.69094 11.0439 9.02473 10.9238C9.81031 10.8815 9.9342 10.9068 9.94203 10.9712C9.95275 11.062 9.06932 11.2874 8.82812 11.357C8.21455 11.534 6.60645 11.8913 6.59758 11.8932C6.60115 11.8935 7.06249 11.9257 7.65531 11.8735C7.89632 11.8522 8.81142 11.7624 9.49557 11.6123C9.49557 11.6123 10.3297 11.4338 10.7759 11.2693C11.2429 11.0973 11.497 10.9512 11.6113 10.7443C11.6063 10.7019 11.6465 10.5516 11.4313 10.4613C10.8807 10.2304 10.2423 10.2721 8.9789 10.2453C7.57789 10.1972 7.11184 9.9626 6.86356 9.77373C6.62548 9.58212 6.74518 9.05204 7.76528 8.5851C8.27917 8.33646 10.2935 7.87759 10.2935 7.87759C9.61511 7.54227 8.35014 6.95284 8.09005 6.82552C7.86199 6.71388 7.49701 6.54572 7.4179 6.34233C7.32824 6.14709 7.6297 5.97888 7.79813 5.9307C8.34057 5.77424 9.10635 5.67701 9.8033 5.66609C10.1536 5.66061 10.2105 5.63806 10.2105 5.63806C10.6939 5.55787 11.0121 5.22722 10.8795 4.70348Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2716_663">
<rect width="12" height="12" fill="white" transform="translate(2.5 2)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.6725 13.9985C3.36161 13.9982 3.06354 13.8746 2.84371 13.6548C2.62388 13.435 2.50026 13.1369 2.5 12.826V7.494C2.5 6.8325 2.7675 6.185 3.2365 5.7165L6.219 2.736C6.45192 2.50247 6.72867 2.31724 7.03335 2.19094C7.33804 2.06464 7.66467 1.99975 7.9945 2H13.3275C13.6384 2.00027 13.9365 2.12388 14.1563 2.34371C14.3761 2.56354 14.4997 2.86162 14.5 3.1725V8.5045C14.4983 9.17074 14.2336 9.80936 13.7635 10.2815L10.781 13.264C10.5477 13.4976 10.2706 13.6829 9.96561 13.8092C9.66059 13.9355 9.33364 14.0003 9.0035 14V13.9985H3.6725ZM8.157 10.5715H5.243V11.257H8.157V10.5715ZM4.4815 5.257H11.243V12.0165L13.3715 9.888C13.7373 9.52036 13.9433 9.02316 13.9445 8.5045V3.1725C13.9445 2.8335 13.6685 2.5555 13.3275 2.5555H7.9945C7.73753 2.55499 7.483 2.6053 7.24556 2.70356C7.00813 2.80181 6.79246 2.94606 6.611 3.128L4.4815 5.257ZM4.3855 5.353L3.628 6.11C3.26258 6.47809 3.0569 6.97533 3.0555 7.494V12.826C3.0555 13.165 3.3315 13.443 3.6725 13.443H9.0055C9.26249 13.4434 9.51701 13.3929 9.75445 13.2946C9.99188 13.1963 10.2075 13.052 10.389 12.87L11.145 12.1145H4.3855V5.353Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 8.01611C13.0945 7.87619 12.9911 7.79551 12.8642 7.8356L4.13456 10.6038C4.00742 10.6441 3.90427 10.7904 3.90427 10.9301V13.7593C3.90427 13.8992 4.00742 13.9801 4.13456 13.9398L12.8642 11.1719C12.9911 11.1315 13.0945 10.9852 13.0945 10.8453V8.01611Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.90427 7.92597C3.90427 8.06588 4.00742 8.21218 4.13456 8.25252L12.8655 11.0209C12.9926 11.0613 13.0958 10.9803 13.0958 10.8407V8.01124C13.0958 7.87158 12.9926 7.72529 12.8655 7.68494L4.13456 4.91652C4.00742 4.87618 3.90427 4.95686 3.90427 5.09677V7.92597Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 2.20248C13.0945 2.06256 12.9911 1.98163 12.8642 2.02197L4.13456 4.78988C4.00742 4.83022 3.90427 4.97652 3.90427 5.11644V7.94563C3.90427 8.08554 4.00742 8.16622 4.13456 8.12614L12.8642 5.35797C12.9911 5.31763 13.0945 5.17133 13.0945 5.03167V2.20248Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0094 13.9181C11.1984 13.9917 11.4139 13.987 11.6047 13.8952L14.0753 12.7064C14.3349 12.5814 14.5 12.3187 14.5 12.0305V3.9696C14.5 3.68136 14.3349 3.41862 14.0753 3.2937L11.6047 2.10485C11.3543 1.98438 11.0614 2.01389 10.8416 2.17363C10.8102 2.19645 10.7803 2.22193 10.7523 2.25001L6.02261 6.56498L3.96246 5.00115C3.77068 4.85558 3.50244 4.86751 3.32432 5.02953L2.66356 5.63059C2.44569 5.82877 2.44544 6.17152 2.66302 6.37004L4.44965 8.00001L2.66302 9.62998C2.44544 9.82849 2.44569 10.1713 2.66356 10.3694L3.32432 10.9705C3.50244 11.1325 3.77068 11.1444 3.96246 10.9989L6.02261 9.43504L10.7523 13.75C10.8271 13.8249 10.915 13.8812 11.0094 13.9181ZM11.5018 5.27587L7.91309 8.00001L11.5018 10.7241V5.27587Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View File

@@ -1,8 +1,5 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 1C2.44771 1 2 1.44772 2 2V13C2 13.5523 2.44772 14 3 14H10.5C10.7761 14 11 13.7761 11 13.5C11 13.2239 10.7761 13 10.5 13H3V2L10.5 2C10.7761 2 11 1.77614 11 1.5C11 1.22386 10.7761 1 10.5 1H3ZM12.6036 4.89645C12.4083 4.70118 12.0917 4.70118 11.8964 4.89645C11.7012 5.09171 11.7012 5.40829 11.8964 5.60355L13.2929 7H6.5C6.22386 7 6 7.22386 6 7.5C6 7.77614 6.22386 8 6.5 8H13.2929L11.8964 9.39645C11.7012 9.59171 11.7012 9.90829 11.8964 10.1036C12.0917 10.2988 12.4083 10.2988 12.6036 10.1036L14.8536 7.85355C15.0488 7.65829 15.0488 7.34171 14.8536 7.14645L12.6036 4.89645Z"
fill="currentColor"
/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.437 11.0461L13.4831 8L10.437 4.95392" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 8L8 8" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.6553 13.4659H4.21843C3.89528 13.4659 3.58537 13.3375 3.35687 13.109C3.12837 12.8805 3 12.5706 3 12.2475V3.71843C3 3.39528 3.12837 3.08537 3.35687 2.85687C3.58537 2.62837 3.89528 2.5 4.21843 2.5H6.6553" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 637 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.628 11.0743V10.4575H8.45562L8.65084 10.2445C8.75911 10.1264 8.96952 9.79454 9.11862 9.50789C9.52153 8.73047 9.51798 7.25107 9.11862 6.43992C8.58614 5.35722 7.49453 4.56381 6.24942 4.35703C4.59252 4.08192 2.86196 5.00312 2.14045 6.54287C1.77038 7.33182 1.77038 8.64437 2.14045 9.43333C2.45905 10.1122 3.11309 10.8204 3.73609 11.1595C4.51439 11.5828 5.18264 11.676 7.51312 11.6848L9.62627 11.6928L9.628 11.0743ZM5.30605 10.169C4.24109 10.0111 3.45215 9.07124 3.45659 7.96813C3.45659 7.33004 3.70064 6.80022 4.18697 6.36182C4.67685 5.91986 5.1312 5.77344 5.86602 5.82048C7.00287 5.89236 7.82382 6.79845 7.82382 7.98056C7.82382 8.61332 7.71996 8.91682 7.33036 9.42534C6.90172 9.98444 6.08345 10.2853 5.30692 10.1699M15.1374 10.9802V10.2684H11.8138V4.47509H10.1986V11.6928H15.1374V10.9802Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View File

@@ -1,3 +1,5 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.72742 8.83338C3.63539 8.57302 3.34973 8.43656 3.08937 8.52858C2.82901 8.6206 2.69255 8.90626 2.78458 9.16662C2.86101 9.38288 2.95188 9.59228 3.056 9.79364C3.81427 11.2601 5.27842 12.3044 7.00014 12.4753L7.00014 14L5.50014 14C5.22399 14 5.00014 14.2239 5.00014 14.5C5.00014 14.7761 5.22399 15 5.50014 15L7.50014 15L9.50014 15C9.77628 15 10.0001 14.7761 10.0001 14.5C10.0001 14.2239 9.77628 14 9.50014 14L8.00014 14L8.00014 12.4753C9.72168 12.3043 11.1857 11.26 11.9439 9.79364C12.048 9.59228 12.1389 9.38288 12.2153 9.16662C12.3073 8.90626 12.1709 8.6206 11.9105 8.52858C11.6501 8.43656 11.3645 8.57302 11.2725 8.83338C11.2114 9.00607 11.1388 9.17337 11.0556 9.33433C10.3899 10.6218 9.04706 11.5 7.49994 11.5C5.95282 11.5 4.60997 10.6218 3.94428 9.33433C3.86104 9.17337 3.78845 9.00607 3.72742 8.83338ZM5.5 3.5L5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5L9.5 3.5C9.5 2.39543 8.60457 1.5 7.5 1.5C6.39543 1.5 5.5 2.39543 5.5 3.5ZM4.5 7.5C4.5 9.15685 5.84315 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5L10.5 3.5C10.5 1.84315 9.15685 0.5 7.5 0.5C5.84315 0.5 4.5 1.84315 4.5 3.5L4.5 7.5Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 12.2028V14.3042" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.2027 6.94928V8.11672C12.2027 9.20041 11.7599 10.2397 10.9717 11.006C10.1836 11.7723 9.11457 12.2028 7.99992 12.2028C6.88527 12.2028 5.81627 11.7723 5.02809 11.006C4.23991 10.2397 3.79712 9.20041 3.79712 8.11672V6.94928" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.1015 3.63555C10.1015 2.56426 9.16065 1.6958 8.00008 1.6958C6.83951 1.6958 5.89868 2.56426 5.89868 3.63555V8.16165C5.89868 9.23294 6.83951 10.1014 8.00008 10.1014C9.16065 10.1014 10.1015 9.23294 10.1015 8.16165V3.63555Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -1,3 +1,8 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.87 1.83637C13.0557 1.63204 13.0407 1.31581 12.8363 1.13006C12.632 0.944307 12.3158 0.959365 12.13 1.16369L10.4589 3.00199C10.2216 1.58215 8.98719 0.5 7.5 0.5C5.84315 0.5 4.5 1.84315 4.5 3.5L4.5 7.5C4.5 8.0754 4.66199 8.61297 4.94286 9.06958L4.24966 9.8321C4.1363 9.6744 4.03412 9.5081 3.94428 9.33433C3.86104 9.17337 3.78845 9.00607 3.72742 8.83338C3.63539 8.57302 3.34973 8.43656 3.08937 8.52858C2.82901 8.6206 2.69255 8.90626 2.78458 9.16662C2.86101 9.38288 2.95188 9.59228 3.056 9.79364C3.20094 10.074 3.37167 10.3388 3.56506 10.5852L2.13003 12.1637C1.94428 12.368 1.95933 12.6842 2.16366 12.87C2.36799 13.0558 2.68422 13.0407 2.86997 12.8364L4.25951 11.3079C5.01297 11.9497 5.95951 12.372 7.00014 12.4753L7.00014 14L5.50014 14C5.22399 14 5.00014 14.2239 5.00014 14.5C5.00014 14.7761 5.22399 15 5.50014 15L7.50014 15L9.50014 15C9.77628 15 10.0001 14.7761 10.0001 14.5C10.0001 14.2239 9.77628 14 9.50014 14L8.00014 14L8.00014 12.4753C9.72168 12.3043 11.1857 11.26 11.9439 9.79364C12.048 9.59228 12.1389 9.38288 12.2153 9.16662C12.3073 8.90626 12.1709 8.6206 11.9105 8.52858C11.6501 8.43656 11.3645 8.57302 11.2725 8.83338C11.2114 9.00607 11.1388 9.17337 11.0556 9.33433C10.3899 10.6218 9.04706 11.5 7.49994 11.5C6.523 11.5 5.62751 11.1498 4.93254 10.5675L5.60604 9.82669C6.12251 10.2476 6.78178 10.5 7.5 10.5C9.15685 10.5 10.5 9.15685 10.5 7.5L10.5 4.44333L12.87 1.83637ZM9.5 4.05673L9.5 3.5C9.5 2.39543 8.60457 1.5 7.5 1.5C6.39543 1.5 5.5 2.39543 5.5 3.5L5.5 7.5C5.5 7.77755 5.55653 8.04189 5.65872 8.28214L9.5 4.05673ZM6.28022 9.08509L9.5 5.54333L9.5 7.5C9.5 8.60457 8.60457 9.5 7.5 9.5C7.04083 9.5 6.6178 9.34527 6.28022 9.08509Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3L13 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 9C12 8.74858 12 8.49375 12 8.23839V7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.00043 7V8.09869C3.98856 8.86731 4.22157 9.62164 4.66938 10.2643C5.11718 10.907 5.75924 11.4085 6.51267 11.7042C7.2661 11.9999 8.09632 12.0761 8.89619 11.923C9.47851 11.8115 10.0253 11.5823 10.5 11.2539" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 6V3.62904C9.99714 3.26103 9.8347 2.90448 9.53885 2.6168C9.24299 2.32913 8.83093 2.12707 8.36903 2.04316C7.90713 1.95926 7.42226 1.9984 6.99252 2.15427C6.56278 2.31015 6.21317 2.57369 6 2.90245" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 6V8.00088C6.00031 8.39636 6.10356 8.78287 6.29674 9.11159C6.48991 9.44031 6.76433 9.69649 7.08534 9.84779C7.40634 9.99909 7.75954 10.0387 8.10032 9.96165C8.4411 9.88459 8.75417 9.69431 9 9.41483" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 12V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,8 +1,5 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 3.25C1 3.11193 1.11193 3 1.25 3H13.75C13.8881 3 14 3.11193 14 3.25V10.75C14 10.8881 13.8881 11 13.75 11H1.25C1.11193 11 1 10.8881 1 10.75V3.25ZM1.25 2C0.559643 2 0 2.55964 0 3.25V10.75C0 11.4404 0.559644 12 1.25 12H5.07341L4.82991 13.2986C4.76645 13.6371 5.02612 13.95 5.37049 13.95H9.62951C9.97389 13.95 10.2336 13.6371 10.1701 13.2986L9.92659 12H13.75C14.4404 12 15 11.4404 15 10.75V3.25C15 2.55964 14.4404 2 13.75 2H1.25ZM9.01091 12H5.98909L5.79222 13.05H9.20778L9.01091 12Z"
fill="currentColor"
/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.8 3H3.2C2.53726 3 2 3.51167 2 4.14286V9.85714C2 10.4883 2.53726 11 3.2 11H12.8C13.4627 11 14 10.4883 14 9.85714V4.14286C14 3.51167 13.4627 3 12.8 3Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.33325 14H10.6666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 11.3333V14" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 677 B

After

Width:  |  Height:  |  Size: 569 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="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -232,7 +232,7 @@
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
@@ -495,7 +495,7 @@
"shift-f12": "editor::GoToImplementation",
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
"ctrl-|": "editor::MoveToEnclosingBracket",
"ctrl-{": "editor::Fold",
"ctrl-}": "editor::UnfoldLines",
@@ -598,6 +598,7 @@
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
@@ -872,8 +873,6 @@
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend",
"alt-enter": "menu::SecondaryConfirm",
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
@@ -910,7 +909,9 @@
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"ctrl-space": "git::StageAll",
"ctrl-shift-space": "git::UnstageAll"
"ctrl-shift-space": "git::UnstageAll",
"ctrl-enter": "git::Commit",
"ctrl-shift-enter": "git::Amend"
}
},
{

View File

@@ -272,7 +272,7 @@
"cmd-n": "agent::NewThread",
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-alt-c": "agent::OpenSettings",
"cmd-alt-p": "agent::OpenRulesLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
@@ -549,7 +549,7 @@
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"cmd-|": "editor::MoveToEnclosingBracket",
"ctrl-m": "editor::MoveToEnclosingBracket",
"ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
@@ -665,6 +665,7 @@
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-t": "theme_selector::Toggle",
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
@@ -950,8 +951,6 @@
"tab": "git_panel::FocusEditor",
"shift-tab": "git_panel::FocusEditor",
"escape": "git_panel::ToggleFocus",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend",
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
"delete": ["git::RestoreFile", { "skip_prompt": false }],
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
@@ -1001,7 +1000,9 @@
"ctrl-g backspace": "git::RestoreTrackedFiles",
"ctrl-g shift-backspace": "git::TrashUntrackedFiles",
"cmd-ctrl-y": "git::StageAll",
"cmd-ctrl-shift-y": "git::UnstageAll"
"cmd-ctrl-shift-y": "git::UnstageAll",
"cmd-enter": "git::Commit",
"cmd-shift-enter": "git::Amend"
}
},
{

View File

@@ -8,7 +8,7 @@
"ctrl-shift-i": "agent::ToggleFocus",
"ctrl-l": "agent::ToggleFocus",
"ctrl-shift-l": "agent::ToggleFocus",
"ctrl-shift-j": "agent::OpenConfiguration"
"ctrl-shift-j": "agent::OpenSettings"
}
},
{

View File

@@ -4,6 +4,7 @@
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom
"ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause",
"f7": "debugger::StepInto",
@@ -44,8 +45,8 @@
"ctrl-alt-right": "pane::GoForward",
"alt-f7": "editor::FindAllReferences",
"ctrl-alt-f7": "editor::FindAllReferences",
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
"ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
"ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
"ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
@@ -94,18 +95,33 @@
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"alt-shift-f10": "task::Spawn",
"ctrl-e": "file_finder::Toggle",
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"ctrl-alt-shift-n": "project_symbols::Toggle",
"alt-0": "git_panel::ToggleFocus",
"alt-1": "workspace::ToggleLeftDock",
"alt-1": "project_panel::ToggleFocus",
"alt-5": "debug_panel::ToggleFocus",
"alt-6": "diagnostics::Deploy",
"alt-7": "outline_panel::ToggleFocus"
}
},
{
"context": "Pane", // this is to override the default Pane mappings to switch tabs
"bindings": {
"alt-1": "project_panel::ToggleFocus",
"alt-2": null, // Bookmarks (left dock)
"alt-3": null, // Find Panel (bottom dock)
"alt-4": null, // Run Panel (bottom dock)
"alt-5": "debug_panel::ToggleFocus",
"alt-6": "diagnostics::Deploy",
"alt-7": "outline_panel::ToggleFocus",
"alt-8": null, // Services (bottom dock)
"alt-9": null, // Git History (bottom dock)
"alt-0": "git_panel::ToggleFocus"
}
},
{
"context": "Workspace || Editor",
"bindings": {
@@ -151,6 +167,9 @@
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
{
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
"bindings": { "escape": "editor::ToggleFocus" }
"bindings": {
"escape": "editor::ToggleFocus",
"shift-escape": "workspace::CloseActiveDock"
}
}
]

View File

@@ -8,7 +8,7 @@
"cmd-shift-i": "agent::ToggleFocus",
"cmd-l": "agent::ToggleFocus",
"cmd-shift-l": "agent::ToggleFocus",
"cmd-shift-j": "agent::OpenConfiguration"
"cmd-shift-j": "agent::OpenSettings"
}
},
{

View File

@@ -4,6 +4,7 @@
"cmd-{": "pane::ActivatePreviousItem",
"cmd-}": "pane::ActivateNextItem",
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
"shift-escape": null, // Unmap workspace::zoom
"ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause",
"f7": "debugger::StepInto",
@@ -96,7 +97,7 @@
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-alt-r": "task::Spawn",
"cmd-e": "file_finder::Toggle",
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
@@ -108,6 +109,21 @@
"cmd-7": "outline_panel::ToggleFocus"
}
},
{
"context": "Pane", // this is to override the default Pane mappings to switch tabs
"bindings": {
"cmd-1": "project_panel::ToggleFocus",
"cmd-2": null, // Bookmarks (left dock)
"cmd-3": null, // Find Panel (bottom dock)
"cmd-4": null, // Run Panel (bottom dock)
"cmd-5": "debug_panel::ToggleFocus",
"cmd-6": "diagnostics::Deploy",
"cmd-7": "outline_panel::ToggleFocus",
"cmd-8": null, // Services (bottom dock)
"cmd-9": null, // Git History (bottom dock)
"cmd-0": "git_panel::ToggleFocus"
}
},
{
"context": "Workspace || Editor",
"bindings": {
@@ -146,11 +162,15 @@
}
},
{ "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
{ "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
{ "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } },
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
{
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
"bindings": { "escape": "editor::ToggleFocus" }
"bindings": {
"escape": "editor::ToggleFocus",
"shift-escape": "workspace::CloseActiveDock"
}
}
]

View File

@@ -1877,5 +1877,25 @@
"save_breakpoints": true,
"dock": "bottom",
"button": true
}
},
// Configures any number of settings profiles that are temporarily applied on
// top of your existing user settings when selected from
// `settings profile selector: toggle`.
// Examples:
// "profiles": {
// "Presenting": {
// "agent_font_size": 20.0,
// "buffer_font_size": 20.0,
// "theme": "One Light",
// "ui_font_size": 20.0
// },
// "Python (ty)": {
// "languages": {
// "Python": {
// "language_servers": ["ty"]
// }
// }
// }
// }
"profiles": []
}

View File

@@ -166,6 +166,7 @@ pub struct ToolCall {
pub content: Vec<ToolCallContent>,
pub status: ToolCallStatus,
pub locations: Vec<acp::ToolCallLocation>,
pub raw_input: Option<serde_json::Value>,
}
impl ToolCall {
@@ -193,6 +194,50 @@ impl ToolCall {
.collect(),
locations: tool_call.locations,
status,
raw_input: tool_call.raw_input,
}
}
fn update(
&mut self,
fields: acp::ToolCallUpdateFields,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
let acp::ToolCallUpdateFields {
kind,
status,
label,
content,
locations,
raw_input,
} = fields;
if let Some(kind) = kind {
self.kind = kind;
}
if let Some(status) = status {
self.status = ToolCallStatus::Allowed { status };
}
if let Some(label) = label {
self.label = cx.new(|cx| Markdown::new_text(label.into(), cx));
}
if let Some(content) = content {
self.content = content
.into_iter()
.map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
.collect();
}
if let Some(locations) = locations {
self.locations = locations;
}
if let Some(raw_input) = raw_input {
self.raw_input = Some(raw_input);
}
}
@@ -238,6 +283,7 @@ impl Display for ToolCallStatus {
match self {
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
ToolCallStatus::Allowed { status } => match status {
acp::ToolCallStatus::Pending => "Pending",
acp::ToolCallStatus::InProgress => "In Progress",
acp::ToolCallStatus::Completed => "Completed",
acp::ToolCallStatus::Failed => "Failed",
@@ -345,7 +391,7 @@ impl ToolCallContent {
cx: &mut App,
) -> Self {
match content {
acp::ToolCallContent::ContentBlock { content } => Self::ContentBlock {
acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock {
content: ContentBlock::new(content, &language_registry, cx),
},
acp::ToolCallContent::Diff { diff } => Self::Diff {
@@ -534,6 +580,9 @@ pub struct AcpThread {
pub enum AcpThreadEvent {
NewEntry,
EntryUpdated(usize),
ToolAuthorizationRequired,
Stopped,
Error,
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
@@ -630,12 +679,62 @@ impl AcpThread {
false
}
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
self.entries.push(entry);
cx.emit(AcpThreadEvent::NewEntry);
pub fn used_tools_since_last_user_message(&self) -> bool {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(..) => return false,
AgentThreadEntry::AssistantMessage(..) => continue,
AgentThreadEntry::ToolCall(..) => return true,
}
}
false
}
pub fn push_assistant_chunk(
pub fn handle_session_update(
&mut self,
update: acp::SessionUpdate,
cx: &mut Context<Self>,
) -> Result<()> {
match update {
acp::SessionUpdate::UserMessage(content_block) => {
self.push_user_content_block(content_block, cx);
}
acp::SessionUpdate::AgentMessageChunk(content_block) => {
self.push_assistant_content_block(content_block, false, cx);
}
acp::SessionUpdate::AgentThoughtChunk(content_block) => {
self.push_assistant_content_block(content_block, true, cx);
}
acp::SessionUpdate::ToolCall(tool_call) => {
self.upsert_tool_call(tool_call, cx);
}
acp::SessionUpdate::ToolCallUpdate(tool_call_update) => {
self.update_tool_call(tool_call_update, cx)?;
}
acp::SessionUpdate::Plan(plan) => {
self.update_plan(plan, cx);
}
}
Ok(())
}
pub fn push_user_content_block(&mut self, chunk: acp::ContentBlock, cx: &mut Context<Self>) {
let language_registry = self.project.read(cx).languages().clone();
let entries_len = self.entries.len();
if let Some(last_entry) = self.entries.last_mut()
&& let AgentThreadEntry::UserMessage(UserMessage { content }) = last_entry
{
content.append(chunk, &language_registry, cx);
cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
} else {
let content = ContentBlock::new(chunk, &language_registry, cx);
self.push_entry(AgentThreadEntry::UserMessage(UserMessage { content }), cx);
}
}
pub fn push_assistant_content_block(
&mut self,
chunk: acp::ContentBlock,
is_thought: bool,
@@ -678,23 +777,22 @@ impl AcpThread {
}
}
fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
self.entries.push(entry);
cx.emit(AcpThreadEvent::NewEntry);
}
pub fn update_tool_call(
&mut self,
id: acp::ToolCallId,
status: acp::ToolCallStatus,
content: Option<Vec<acp::ToolCallContent>>,
update: acp::ToolCallUpdate,
cx: &mut Context<Self>,
) -> Result<()> {
let languages = self.project.read(cx).languages().clone();
let (ix, current_call) = self.tool_call_mut(&id).context("Tool call not found")?;
if let Some(content) = content {
current_call.content = content
.into_iter()
.map(|chunk| ToolCallContent::from_acp(chunk, languages.clone(), cx))
.collect();
}
current_call.status = ToolCallStatus::Allowed { status };
let (ix, current_call) = self
.tool_call_mut(&update.id)
.context("Tool call not found")?;
current_call.update(update.fields, languages, cx);
cx.emit(AcpThreadEvent::EntryUpdated(ix));
@@ -751,6 +849,37 @@ impl AcpThread {
})
}
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
return;
};
let buffer = project.open_buffer(path, cx);
cx.spawn(async move |project, cx| {
let buffer = buffer.await?;
project.update(cx, |project, cx| {
let position = if let Some(line) = location.line {
let snapshot = buffer.read(cx).snapshot();
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
snapshot.anchor_before(point)
} else {
Anchor::MIN
};
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})
})
.detach_and_log_err(cx);
});
}
pub fn request_tool_call_permission(
&mut self,
tool_call: acp::ToolCall,
@@ -765,6 +894,7 @@ impl AcpThread {
};
self.upsert_tool_call_inner(tool_call, status, cx);
cx.emit(AcpThreadEvent::ToolAuthorizationRequired);
rx
}
@@ -801,6 +931,25 @@ impl AcpThread {
cx.emit(AcpThreadEvent::EntryUpdated(ix));
}
/// Returns true if the last turn is awaiting tool authorization
pub fn waiting_for_tool_confirmation(&self) -> bool {
for entry in self.entries.iter().rev() {
match &entry {
AgentThreadEntry::ToolCall(call) => match call.status {
ToolCallStatus::WaitingForConfirmation { .. } => return true,
ToolCallStatus::Allowed { .. }
| ToolCallStatus::Rejected
| ToolCallStatus::Canceled => continue,
},
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
// Reached the beginning of the turn
return false;
}
}
}
false
}
pub fn plan(&self) -> &Plan {
&self.plan
}
@@ -824,56 +973,6 @@ impl AcpThread {
cx.notify();
}
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
return;
};
let buffer = project.open_buffer(path, cx);
cx.spawn(async move |project, cx| {
let buffer = buffer.await?;
project.update(cx, |project, cx| {
let position = if let Some(line) = location.line {
let snapshot = buffer.read(cx).snapshot();
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
snapshot.anchor_before(point)
} else {
Anchor::MIN
};
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})
})
.detach_and_log_err(cx);
});
}
/// Returns true if the last turn is awaiting tool authorization
pub fn waiting_for_tool_confirmation(&self) -> bool {
for entry in self.entries.iter().rev() {
match &entry {
AgentThreadEntry::ToolCall(call) => match call.status {
ToolCallStatus::WaitingForConfirmation { .. } => return true,
ToolCallStatus::Allowed { .. }
| ToolCallStatus::Rejected
| ToolCallStatus::Canceled => continue,
},
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
// Reached the beginning of the turn
return false;
}
}
}
false
}
pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future<Output = Result<()>> {
self.connection.authenticate(cx)
}
@@ -919,7 +1018,7 @@ impl AcpThread {
let result = this
.update(cx, |this, cx| {
this.connection.prompt(
acp::PromptToolArguments {
acp::PromptArguments {
prompt: message,
session_id: this.session_id.clone(),
},
@@ -935,12 +1034,18 @@ impl AcpThread {
.log_err();
}));
async move {
match rx.await {
Ok(Err(e)) => Err(e)?,
_ => Ok(()),
cx.spawn(async move |this, cx| match rx.await {
Ok(Err(e)) => {
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error))
.log_err();
Err(e)?
}
}
_ => {
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped))
.log_err();
Ok(())
}
})
.boxed()
}
@@ -1148,7 +1253,87 @@ mod tests {
}
#[gpui::test]
async fn test_thinking_concatenation(cx: &mut TestAppContext) {
async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let (thread, _fake_server) = fake_acp_thread(project, cx);
// Test creating a new user message
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Hello, ".to_string(),
}),
cx,
);
});
thread.update(cx, |thread, cx| {
assert_eq!(thread.entries.len(), 1);
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
assert_eq!(user_msg.content.to_markdown(cx), "Hello, ");
} else {
panic!("Expected UserMessage");
}
});
// Test appending to existing user message
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "world!".to_string(),
}),
cx,
);
});
thread.update(cx, |thread, cx| {
assert_eq!(thread.entries.len(), 1);
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!");
} else {
panic!("Expected UserMessage");
}
});
// Test creating new user message after assistant message
thread.update(cx, |thread, cx| {
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Assistant response".to_string(),
}),
false,
cx,
);
});
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "New user message".to_string(),
}),
cx,
);
});
thread.update(cx, |thread, cx| {
assert_eq!(thread.entries.len(), 3);
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] {
assert_eq!(user_msg.content.to_markdown(cx), "New user message");
} else {
panic!("Expected UserMessage at index 2");
}
});
}
#[gpui::test]
async fn test_thinking_concatenation(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
@@ -1434,6 +1619,7 @@ mod tests {
name: "test",
connection,
child_status: io_task,
current_thread: thread_rc,
};
AcpThread::new(

View File

@@ -20,7 +20,7 @@ pub trait AgentConnection {
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>>;
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>>;
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
}

View File

@@ -1,4 +1,4 @@
///! Translates old acp agents into the new schema
// Translates old acp agents into the new schema
use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
use anyhow::{Context as _, Result};
@@ -7,8 +7,9 @@ use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
use ui::App;
use util::ResultExt as _;
use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
use crate::{AcpThread, AgentConnection};
#[derive(Clone)]
pub struct OldAcpClientDelegate {
@@ -40,13 +41,13 @@ impl acp_old::Client for OldAcpClientDelegate {
.borrow()
.update(cx, |thread, cx| match params.chunk {
acp_old::AssistantMessageChunk::Text { text } => {
thread.push_assistant_chunk(text.into(), false, cx)
thread.push_assistant_content_block(text.into(), false, cx)
}
acp_old::AssistantMessageChunk::Thought { thought } => {
thread.push_assistant_chunk(thought.into(), true, cx)
thread.push_assistant_content_block(thought.into(), true, cx)
}
})
.ok();
.log_err();
})?;
Ok(())
@@ -182,31 +183,23 @@ impl acp_old::Client for OldAcpClientDelegate {
cx.update(|cx| {
self.thread.borrow().update(cx, |thread, cx| {
let languages = thread.project.read(cx).languages().clone();
if let Some((ix, tool_call)) = thread
.tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
{
tool_call.status = ToolCallStatus::Allowed {
status: into_new_tool_call_status(request.status),
};
tool_call.content = request
.content
.into_iter()
.map(|content| {
ToolCallContent::from_acp(
into_new_tool_call_content(content),
languages.clone(),
cx,
)
})
.collect();
cx.emit(AcpThreadEvent::EntryUpdated(ix));
anyhow::Ok(())
} else {
anyhow::bail!("Tool call not found")
}
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")??;
@@ -285,6 +278,7 @@ fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams)
.into_iter()
.map(into_new_tool_call_location)
.collect(),
raw_input: None,
}
}
@@ -311,12 +305,7 @@ fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallSt
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
match content {
acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
content: acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: markdown,
}),
},
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
diff: into_new_diff(diff),
},
@@ -376,6 +365,7 @@ pub struct OldAcpAgentConnection {
pub name: &'static str,
pub connection: acp_old::AgentConnection,
pub child_status: Task<Result<()>>,
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
}
impl AgentConnection for OldAcpAgentConnection {
@@ -395,6 +385,7 @@ impl AgentConnection for OldAcpAgentConnection {
}
.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)?;
@@ -408,6 +399,7 @@ impl AgentConnection for OldAcpAgentConnection {
let session_id = acp::SessionId("acp-old-no-id".into());
AcpThread::new(self.clone(), project, session_id, cx)
});
current_thread.replace(thread.downgrade());
thread
})
})
@@ -423,7 +415,7 @@ impl AgentConnection for OldAcpAgentConnection {
})
}
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
let chunks = params
.prompt
.into_iter()

View File

@@ -25,6 +25,7 @@ assistant_context.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
component.workspace = true
context_server.workspace = true
@@ -35,9 +36,9 @@ futures.workspace = true
git.workspace = true
gpui.workspace = true
heed.workspace = true
http_client.workspace = true
icons.workspace = true
indoc.workspace = true
http_client.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
@@ -46,7 +47,6 @@ paths.workspace = true
postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
ref-cast.workspace = true
rope.workspace = true
schemars.workspace = true
@@ -63,7 +63,6 @@ time.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
zstd.workspace = true
[dev-dependencies]

View File

@@ -308,7 +308,12 @@ mod tests {
unimplemented!()
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
fn needs_confirmation(
&self,
_input: &serde_json::Value,
_project: &Entity<Project>,
_cx: &App,
) -> bool {
unimplemented!()
}

View File

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

View File

@@ -13,6 +13,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::HashMap;
use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
@@ -36,7 +37,6 @@ use project::{
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
};
use prompt_store::{ModelContext, PromptBuilder};
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -49,7 +49,6 @@ use std::{
use thiserror::Error;
use util::{ResultExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
const MAX_RETRY_ATTEMPTS: u8 = 4;
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
@@ -942,7 +941,7 @@ impl Thread {
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
self.tool_use.tool_uses_for_message(id, cx)
self.tool_use.tool_uses_for_message(id, &self.project, cx)
}
pub fn tool_results_for_message(
@@ -1681,7 +1680,7 @@ impl Thread {
let completion_mode = request
.mode
.unwrap_or(zed_llm_client::CompletionMode::Normal);
.unwrap_or(cloud_llm_client::CompletionMode::Normal);
self.last_received_chunk_at = Some(Instant::now());
@@ -2037,6 +2036,12 @@ impl Thread {
if let Some(retry_strategy) =
Thread::get_retry_strategy(completion_error)
{
log::info!(
"Retrying with {:?} for language model completion error {:?}",
retry_strategy,
completion_error
);
retry_scheduled = thread
.handle_retryable_error_with_delay(
&completion_error,
@@ -2246,15 +2251,14 @@ impl Thread {
..
}
| AuthenticationError { .. }
| PermissionError { .. } => None,
// These errors might be transient, so retry them
SerializeRequest { .. }
| BuildRequestBody { .. }
| PromptTooLarge { .. }
| PermissionError { .. }
| NoApiKey { .. }
| ApiEndpointNotFound { .. }
| NoApiKey { .. } => Some(RetryStrategy::Fixed {
| PromptTooLarge { .. } => None,
// These errors might be transient, so retry them
SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed {
delay: BASE_RETRY_DELAY,
max_attempts: 2,
max_attempts: 1,
}),
// Retry all other 4xx and 5xx errors once.
HttpResponseError { status_code, .. }
@@ -2552,7 +2556,7 @@ impl Thread {
return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx);
}
if tool.needs_confirmation(&tool_use.input, cx)
if tool.needs_confirmation(&tool_use.input, &self.project, cx)
&& !AgentSettings::get_global(cx).always_allow_tool_actions
{
self.tool_use.confirm_tool_use(
@@ -3250,8 +3254,10 @@ impl Thread {
}
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
project.user_store().update(cx, |user_store, cx| {
self.project
.read(cx)
.user_store()
.update(cx, |user_store, cx| {
user_store.update_model_request_usage(
ModelRequestUsage(RequestUsage {
amount: amount as i32,
@@ -3259,8 +3265,7 @@ impl Thread {
}),
cx,
)
})
});
});
}
pub fn deny_tool_use(

View File

@@ -41,6 +41,9 @@ use std::{
};
use util::ResultExt as _;
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]
@@ -874,7 +877,11 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists();
let connection = Connection::open_file(&sqlite_path.to_string_lossy());
let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};
connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads (

View File

@@ -165,7 +165,12 @@ impl ToolUseState {
self.pending_tool_uses_by_id.values().collect()
}
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
pub fn tool_uses_for_message(
&self,
id: MessageId,
project: &Entity<Project>,
cx: &App,
) -> Vec<ToolUse> {
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
return Vec::new();
};
@@ -211,7 +216,10 @@ impl ToolUseState {
let (icon, needs_confirmation) =
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
(
tool.icon(),
tool.needs_confirmation(&tool_use.input, project, cx),
)
} else {
(IconName::Cog, false)
};

View File

@@ -29,6 +29,7 @@ itertools.workspace = true
log.workspace = true
paths.workspace = true
project.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -40,6 +41,7 @@ ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
indoc.workspace = true
which.workspace = true
workspace-hack.workspace = true

View File

@@ -1,11 +1,14 @@
mod claude;
mod codex;
mod gemini;
mod mcp_server;
mod settings;
#[cfg(test)]
mod e2e_tests;
pub use claude::*;
pub use codex::*;
pub use gemini::*;
pub use settings::*;

View File

@@ -2,13 +2,13 @@ mod mcp_server;
pub mod tools;
use collections::HashMap;
use context_server::listener::McpServerTool;
use project::Project;
use settings::SettingsStore;
use smol::process::Child;
use std::cell::RefCell;
use std::fmt::Display;
use std::path::Path;
use std::pin::pin;
use std::rc::Rc;
use uuid::Uuid;
@@ -44,7 +44,7 @@ impl AgentServer for ClaudeCode {
}
fn empty_state_message(&self) -> &'static str {
""
"How can I help you today?"
}
fn logo(&self) -> ui::IconName {
@@ -65,19 +65,6 @@ impl AgentServer for ClaudeCode {
}
}
#[cfg(unix)]
fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
let pid = nix::unistd::Pid::from_raw(pid);
nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
.map_err(|e| anyhow!("Failed to interrupt process: {}", e))
}
#[cfg(windows)]
fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
panic!("Cancel not implemented on Windows")
}
struct ClaudeAgentConnection {
sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
}
@@ -126,7 +113,6 @@ impl AgentConnection for ClaudeAgentConnection {
let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>();
let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
@@ -136,50 +122,28 @@ impl AgentConnection for ClaudeAgentConnection {
let session_id = session_id.clone();
async move {
let mut outgoing_rx = Some(outgoing_rx);
let mut mode = ClaudeSessionMode::Start;
loop {
let mut child = spawn_claude(
&command,
mode,
session_id.clone(),
&mcp_config_path,
&cwd,
)
.await?;
mode = ClaudeSessionMode::Resume;
let mut child = spawn_claude(
&command,
ClaudeSessionMode::Start,
session_id.clone(),
&mcp_config_path,
&cwd,
)
.await?;
let pid = child.id();
log::trace!("Spawned (pid: {})", pid);
let pid = child.id();
log::trace!("Spawned (pid: {})", pid);
let mut io_fut = pin!(
ClaudeAgentSession::handle_io(
outgoing_rx.take().unwrap(),
incoming_message_tx.clone(),
child.stdin.take().unwrap(),
child.stdout.take().unwrap(),
)
.fuse()
);
ClaudeAgentSession::handle_io(
outgoing_rx.take().unwrap(),
incoming_message_tx.clone(),
child.stdin.take().unwrap(),
child.stdout.take().unwrap(),
)
.await?;
select_biased! {
done_tx = cancel_rx.next() => {
if let Some(done_tx) = done_tx {
log::trace!("Interrupted (pid: {})", pid);
let result = send_interrupt(pid as i32);
outgoing_rx.replace(io_fut.await?);
done_tx.send(result).log_err();
continue;
}
}
result = io_fut => {
result?;
}
}
log::trace!("Stopped (pid: {})", pid);
break;
}
log::trace!("Stopped (pid: {})", pid);
drop(mcp_config_path);
anyhow::Ok(())
@@ -212,7 +176,6 @@ impl AgentConnection for ClaudeAgentConnection {
let session = ClaudeAgentSession {
outgoing_tx,
end_turn_tx,
cancel_tx,
_handler_task: handler_task,
_mcp_server: Some(permission_mcp_server),
};
@@ -227,7 +190,7 @@ impl AgentConnection for ClaudeAgentConnection {
Task::ready(Err(anyhow!("Authentication not supported")))
}
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
let sessions = self.sessions.borrow();
let Some(session) = sessions.get(&params.session_id) else {
return Task::ready(Err(anyhow!(
@@ -277,37 +240,24 @@ impl AgentConnection for ClaudeAgentConnection {
})
}
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
let sessions = self.sessions.borrow();
let Some(session) = sessions.get(&session_id) else {
log::warn!("Attempted to cancel nonexistent session {}", session_id);
return;
};
let (done_tx, done_rx) = oneshot::channel();
if session
.cancel_tx
.unbounded_send(done_tx)
.log_err()
.is_some()
{
let end_turn_tx = session.end_turn_tx.clone();
cx.foreground_executor()
.spawn(async move {
done_rx.await??;
if let Some(end_turn_tx) = end_turn_tx.take() {
end_turn_tx.send(Ok(())).ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
session
.outgoing_tx
.unbounded_send(SdkMessage::new_interrupt_message())
.log_err();
}
}
#[derive(Clone, Copy)]
enum ClaudeSessionMode {
Start,
#[expect(dead_code)]
Resume,
}
@@ -332,10 +282,16 @@ async fn spawn_claude(
&format!(
"mcp__{}__{}",
mcp_server::SERVER_NAME,
mcp_server::PERMISSION_TOOL
mcp_server::PermissionTool::NAME,
),
"--allowedTools",
"mcp__zed__Read,mcp__zed__Edit",
&format!(
"mcp__{}__{},mcp__{}__{}",
mcp_server::SERVER_NAME,
mcp_server::EditTool::NAME,
mcp_server::SERVER_NAME,
mcp_server::ReadTool::NAME
),
"--disallowedTools",
"Read,Edit",
])
@@ -357,7 +313,6 @@ async fn spawn_claude(
struct ClaudeAgentSession {
outgoing_tx: UnboundedSender<SdkMessage>,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
_mcp_server: Option<ClaudeZedMcpServer>,
_handler_task: Task<()>,
}
@@ -370,6 +325,8 @@ impl ClaudeAgentSession {
cx: &mut AsyncApp,
) {
match message {
// we should only be sending these out, they don't need to be in the thread
SdkMessage::ControlRequest { .. } => {}
SdkMessage::Assistant {
message,
session_id: _,
@@ -393,7 +350,7 @@ impl ClaudeAgentSession {
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
thread
.update(cx, |thread, cx| {
thread.push_assistant_chunk(text.into(), false, cx)
thread.push_assistant_content_block(text.into(), false, cx)
})
.log_err();
}
@@ -430,9 +387,15 @@ impl ClaudeAgentSession {
thread
.update(cx, |thread, cx| {
thread.update_tool_call(
acp::ToolCallId(tool_use_id.into()),
acp::ToolCallStatus::Completed,
(!content.is_empty()).then(|| vec![content.into()]),
acp::ToolCallUpdate {
id: acp::ToolCallId(tool_use_id.into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
content: (!content.is_empty())
.then(|| vec![content.into()]),
..Default::default()
},
},
cx,
)
})
@@ -445,7 +408,7 @@ impl ClaudeAgentSession {
| ContentChunk::WebSearchToolResult => {
thread
.update(cx, |thread, cx| {
thread.push_assistant_chunk(
thread.push_assistant_content_block(
format!("Unsupported content: {:?}", chunk).into(),
false,
cx,
@@ -457,17 +420,25 @@ impl ClaudeAgentSession {
}
}
SdkMessage::Result {
is_error, subtype, ..
is_error,
subtype,
result,
..
} => {
if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() {
if is_error {
end_turn_tx.send(Err(anyhow!("Error: {subtype}"))).ok();
end_turn_tx
.send(Err(anyhow!(
"Error: {}",
result.unwrap_or_else(|| subtype.to_string())
)))
.ok();
} else {
end_turn_tx.send(Ok(())).ok();
}
}
}
SdkMessage::System { .. } => {}
SdkMessage::System { .. } | SdkMessage::ControlResponse { .. } => {}
}
}
@@ -636,14 +607,12 @@ enum SdkMessage {
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
// A user message
User {
message: Message, // from Anthropic SDK
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
// Emitted as the last message in a conversation
Result {
subtype: ResultErrorType,
@@ -668,6 +637,26 @@ enum SdkMessage {
#[serde(rename = "permissionMode")]
permission_mode: PermissionMode,
},
/// Messages used to control the conversation, outside of chat messages to the model
ControlRequest {
request_id: String,
request: ControlRequest,
},
/// Response to a control request
ControlResponse { response: ControlResponse },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "subtype", rename_all = "snake_case")]
enum ControlRequest {
/// Cancel the current conversation
Interrupt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ControlResponse {
request_id: String,
subtype: ResultErrorType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -688,6 +677,24 @@ impl Display for ResultErrorType {
}
}
impl SdkMessage {
fn new_interrupt_message() -> Self {
use rand::Rng;
// In the Claude Code TS SDK they just generate a random 12 character string,
// `Math.random().toString(36).substring(2, 15)`
let request_id = rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(12)
.map(char::from)
.collect();
Self::ControlRequest {
request_id,
request: ControlRequest::Interrupt,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct McpServer {
name: String,
@@ -708,7 +715,7 @@ pub(crate) mod tests {
use super::*;
use serde_json::json;
crate::common_e2e_tests!(ClaudeCode);
crate::common_e2e_tests!(ClaudeCode, allow_option_id = "allow");
pub fn local_command() -> AgentServerCommand {
AgentServerCommand {

View File

@@ -1,49 +1,24 @@
use std::path::PathBuf;
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use anyhow::{Context, Result};
use collections::HashMap;
use context_server::listener::{McpServerTool, ToolResponse};
use context_server::types::{
CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
ToolResponseContent, ToolsCapabilities, requests,
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
};
use gpui::{App, AsyncApp, Entity, Task, WeakEntity};
use gpui::{App, AsyncApp, Task, WeakEntity};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
pub struct ClaudeZedMcpServer {
server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
pub const READ_TOOL: &str = "Read";
pub const EDIT_TOOL: &str = "Edit";
pub const PERMISSION_TOOL: &str = "Confirmation";
#[derive(Deserialize, JsonSchema, Debug)]
struct PermissionToolParams {
tool_name: String,
input: serde_json::Value,
tool_use_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct PermissionToolResponse {
behavior: PermissionToolBehavior,
updated_input: serde_json::Value,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum PermissionToolBehavior {
Allow,
Deny,
}
impl ClaudeZedMcpServer {
pub async fn new(
@@ -52,18 +27,28 @@ impl ClaudeZedMcpServer {
) -> Result<Self> {
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools);
mcp_server.handle_request::<requests::CallTool>(move |request, cx| {
Self::handle_call_tool(request, thread_rx.clone(), cx)
mcp_server.add_tool(PermissionTool {
thread_rx: thread_rx.clone(),
});
mcp_server.add_tool(ReadTool {
thread_rx: thread_rx.clone(),
});
mcp_server.add_tool(EditTool {
thread_rx: 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![
@@ -96,193 +81,6 @@ impl ClaudeZedMcpServer {
})
})
}
fn handle_list_tools(_: (), cx: &App) -> Task<Result<ListToolsResponse>> {
cx.foreground_executor().spawn(async move {
Ok(ListToolsResponse {
tools: vec![
Tool {
name: PERMISSION_TOOL.into(),
input_schema: schemars::schema_for!(PermissionToolParams).into(),
description: None,
annotations: None,
},
Tool {
name: READ_TOOL.into(),
input_schema: schemars::schema_for!(ReadToolParams).into(),
description: Some("Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents.".to_string()),
annotations: Some(ToolAnnotations {
title: Some("Read file".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
open_world_hint: Some(false),
// if time passes the contents might change, but it's not going to do anything different
// true or false seem too strong, let's try a none.
idempotent_hint: None,
}),
},
Tool {
name: EDIT_TOOL.into(),
input_schema: schemars::schema_for!(EditToolParams).into(),
description: Some("Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better.".to_string()),
annotations: Some(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),
}),
},
],
next_cursor: None,
meta: None,
})
})
}
fn handle_call_tool(
request: CallToolParams,
mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
cx: &App,
) -> Task<Result<CallToolResponse>> {
cx.spawn(async move |cx| {
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
if request.name.as_str() == PERMISSION_TOOL {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
let result = Self::handle_permissions_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&result)?,
}],
is_error: None,
meta: None,
})
} else if request.name.as_str() == READ_TOOL {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
let content = Self::handle_read_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content,
is_error: None,
meta: None,
})
} else if request.name.as_str() == EDIT_TOOL {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
Self::handle_edit_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![],
is_error: None,
meta: None,
})
} else {
anyhow::bail!("Unsupported tool");
}
})
}
fn handle_read_tool_call(
ReadToolParams {
abs_path,
offset,
limit,
}: ReadToolParams,
thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<Vec<ToolResponseContent>>> {
cx.spawn(async move |cx| {
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(abs_path, offset, limit, false, cx)
})?
.await?;
Ok(vec![ToolResponseContent::Text { text: content }])
})
}
fn handle_edit_tool_call(
params: EditToolParams,
thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<()>> {
cx.spawn(async move |cx| {
let content = thread
.update(cx, |threads, cx| {
threads.read_text_file(params.abs_path.clone(), None, None, true, cx)
})?
.await?;
let new_content = content.replace(&params.old_text, &params.new_text);
if new_content == content {
return Err(anyhow::anyhow!("The old_text was not found in the content"));
}
thread
.update(cx, |threads, cx| {
threads.write_text_file(params.abs_path, new_content, cx)
})?
.await?;
Ok(())
})
}
fn handle_permissions_tool_call(
params: PermissionToolParams,
thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<PermissionToolResponse>> {
cx.spawn(async move |cx| {
let claude_tool = ClaudeTool::infer(&params.tool_name, params.input.clone());
let tool_call_id =
acp::ToolCallId(params.tool_use_id.context("Tool ID required")?.into());
let allow_option_id = acp::PermissionOptionId("allow".into());
let reject_option_id = acp::PermissionOptionId("reject".into());
let chosen_option = thread
.update(cx, |thread, cx| {
thread.request_tool_call_permission(
claude_tool.as_acp(tool_call_id),
vec![
acp::PermissionOption {
id: allow_option_id.clone(),
label: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: reject_option_id,
label: "Reject".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
cx,
)
})?
.await?;
if chosen_option == allow_option_id {
Ok(PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: params.input,
})
} else {
Ok(PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: params.input,
})
}
})
}
}
#[derive(Serialize)]
@@ -299,3 +97,206 @@ pub struct McpServerConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
}
// Tools
#[derive(Clone)]
pub struct PermissionTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
#[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 McpServerTool for PermissionTool {
type Input = PermissionToolParams;
type Output = ();
const NAME: &'static str = "Confirmation";
fn description(&self) -> &'static str {
"Request permission for tool calls"
}
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 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());
let allow_option_id = acp::PermissionOptionId("allow".into());
let reject_option_id = acp::PermissionOptionId("reject".into());
let chosen_option = thread
.update(cx, |thread, cx| {
thread.request_tool_call_permission(
claude_tool.as_acp(tool_call_id),
vec![
acp::PermissionOption {
id: allow_option_id.clone(),
label: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: reject_option_id.clone(),
label: "Reject".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
cx,
)
})?
.await?;
let response = if chosen_option == allow_option_id {
PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
}
} else {
debug_assert_eq!(chosen_option, reject_option_id);
PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
}
};
Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
})
}
}
#[derive(Clone)]
pub struct ReadTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for ReadTool {
type Input = ReadToolParams;
type Output = ();
const NAME: &'static str = "Read";
fn description(&self) -> &'static str {
"Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
}
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: (),
})
}
}
#[derive(Clone)]
pub struct EditTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for EditTool {
type Input = EditToolParams;
type Output = ();
const NAME: &'static str = "Edit";
fn description(&self) -> &'static str {
"Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
}
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 = content.replace(&input.old_text, &input.new_text);
if new_content == content {
return Err(anyhow::anyhow!("The old_text was not found in the content"));
}
thread
.update(cx, |thread, cx| {
thread.write_text_file(input.abs_path, new_content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![],
structured_content: (),
})
}
}

View File

@@ -311,6 +311,7 @@ impl ClaudeTool {
label: self.label(),
content: self.content(),
locations: self.locations(),
raw_input: None,
}
}
}

View File

@@ -0,0 +1,319 @@
use agent_client_protocol as acp;
use anyhow::anyhow;
use collections::HashMap;
use context_server::listener::McpServerTool;
use context_server::types::requests;
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
use futures::channel::{mpsc, oneshot};
use project::Project;
use settings::SettingsStore;
use smol::stream::StreamExt as _;
use std::cell::RefCell;
use std::rc::Rc;
use std::{path::Path, sync::Arc};
use util::ResultExt;
use anyhow::{Context, Result};
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use crate::mcp_server::ZedMcpServer;
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server};
use acp_thread::{AcpThread, AgentConnection};
#[derive(Clone)]
pub struct Codex;
impl AgentServer for Codex {
fn name(&self) -> &'static str {
"Codex"
}
fn empty_state_headline(&self) -> &'static str {
"Welcome to Codex"
}
fn empty_state_message(&self) -> &'static str {
"What can I help with?"
}
fn logo(&self) -> ui::IconName {
ui::IconName::AiOpenAi
}
fn connect(
&self,
_root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let project = project.clone();
let working_directory = project.read(cx).active_project_directory(cx);
cx.spawn(async move |cx| {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
})?;
let Some(command) =
AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await
else {
anyhow::bail!("Failed to find codex binary");
};
let client: Arc<ContextServer> = ContextServer::stdio(
ContextServerId("codex-mcp-server".into()),
ContextServerCommand {
path: command.path,
args: command.args,
env: command.env,
},
working_directory,
)
.into();
ContextServer::start(client.clone(), cx).await?;
let (notification_tx, mut notification_rx) = mpsc::unbounded();
client
.client()
.context("Failed to subscribe")?
.on_notification(acp::SESSION_UPDATE_METHOD_NAME, {
move |notification, _cx| {
let notification_tx = notification_tx.clone();
log::trace!(
"ACP Notification: {}",
serde_json::to_string_pretty(&notification).unwrap()
);
if let Some(notification) =
serde_json::from_value::<acp::SessionNotification>(notification)
.log_err()
{
notification_tx.unbounded_send(notification).ok();
}
}
});
let sessions = Rc::new(RefCell::new(HashMap::default()));
let notification_handler_task = cx.spawn({
let sessions = sessions.clone();
async move |cx| {
while let Some(notification) = notification_rx.next().await {
CodexConnection::handle_session_notification(
notification,
sessions.clone(),
cx,
)
}
}
});
let connection = CodexConnection {
client,
sessions,
_notification_handler_task: notification_handler_task,
};
Ok(Rc::new(connection) as _)
})
}
}
struct CodexConnection {
client: Arc<context_server::ContextServer>,
sessions: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
_notification_handler_task: Task<()>,
}
struct CodexSession {
thread: WeakEntity<AcpThread>,
cancel_tx: Option<oneshot::Sender<()>>,
_mcp_server: ZedMcpServer,
}
impl AgentConnection for CodexConnection {
fn name(&self) -> &'static str {
"Codex"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
cwd: &Path,
cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
let client = self.client.client();
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
cx.spawn(async move |cx| {
let client = client.context("MCP server is not initialized yet")?;
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
let mcp_server = ZedMcpServer::new(thread_rx, cx).await?;
let response = client
.request::<requests::CallTool>(context_server::types::CallToolParams {
name: acp::NEW_SESSION_TOOL_NAME.into(),
arguments: Some(serde_json::to_value(acp::NewSessionArguments {
mcp_servers: [(
mcp_server::SERVER_NAME.to_string(),
mcp_server.server_config()?,
)]
.into(),
client_tools: acp::ClientTools {
request_permission: Some(acp::McpToolId {
mcp_server: mcp_server::SERVER_NAME.into(),
tool_name: mcp_server::RequestPermissionTool::NAME.into(),
}),
read_text_file: Some(acp::McpToolId {
mcp_server: mcp_server::SERVER_NAME.into(),
tool_name: mcp_server::ReadTextFileTool::NAME.into(),
}),
write_text_file: Some(acp::McpToolId {
mcp_server: mcp_server::SERVER_NAME.into(),
tool_name: mcp_server::WriteTextFileTool::NAME.into(),
}),
},
cwd,
})?),
meta: None,
})
.await?;
if response.is_error.unwrap_or_default() {
return Err(anyhow!(response.text_contents()));
}
let result = serde_json::from_value::<acp::NewSessionOutput>(
response.structured_content.context("Empty response")?,
)?;
let thread =
cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?;
thread_tx.send(thread.downgrade())?;
let session = CodexSession {
thread: thread.downgrade(),
cancel_tx: None,
_mcp_server: mcp_server,
};
sessions.borrow_mut().insert(result.session_id, session);
Ok(thread)
})
}
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Err(anyhow!("Authentication not supported")))
}
fn prompt(
&self,
params: agent_client_protocol::PromptArguments,
cx: &mut App,
) -> Task<Result<()>> {
let client = self.client.client();
let sessions = self.sessions.clone();
cx.foreground_executor().spawn(async move {
let client = client.context("MCP server is not initialized yet")?;
let (new_cancel_tx, cancel_rx) = oneshot::channel();
{
let mut sessions = sessions.borrow_mut();
let session = sessions
.get_mut(&params.session_id)
.context("Session not found")?;
session.cancel_tx.replace(new_cancel_tx);
}
let result = client
.request_with::<requests::CallTool>(
context_server::types::CallToolParams {
name: acp::PROMPT_TOOL_NAME.into(),
arguments: Some(serde_json::to_value(params)?),
meta: None,
},
Some(cancel_rx),
None,
)
.await;
if let Err(err) = &result
&& err.is::<context_server::client::RequestCanceled>()
{
return Ok(());
}
let response = result?;
if response.is_error.unwrap_or_default() {
return Err(anyhow!(response.text_contents()));
}
Ok(())
})
}
fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) {
let mut sessions = self.sessions.borrow_mut();
if let Some(cancel_tx) = sessions
.get_mut(session_id)
.and_then(|session| session.cancel_tx.take())
{
cancel_tx.send(()).ok();
}
}
}
impl CodexConnection {
pub fn handle_session_notification(
notification: acp::SessionNotification,
threads: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
cx: &mut AsyncApp,
) {
let threads = threads.borrow();
let Some(thread) = threads
.get(&notification.session_id)
.and_then(|session| session.thread.upgrade())
else {
log::error!(
"Thread not found for session ID: {}",
notification.session_id
);
return;
};
thread
.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})
.log_err();
}
}
impl Drop for CodexConnection {
fn drop(&mut self) {
self.client.stop().log_err();
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::AgentServerCommand;
use std::path::Path;
crate::common_e2e_tests!(Codex, allow_option_id = "approve");
pub fn local_command() -> AgentServerCommand {
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../../codex/codex-rs/target/debug/codex");
AgentServerCommand {
path: cli_path,
args: vec![],
env: None,
}
}
}

View File

@@ -1,4 +1,8 @@
use std::{path::Path, sync::Arc, time::Duration};
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
@@ -8,7 +12,6 @@ use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use serde_json::json;
use settings::{Settings, SettingsStore};
use util::path;
@@ -23,7 +26,11 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
.unwrap();
thread.read_with(cx, |thread, _| {
assert_eq!(thread.entries().len(), 2);
assert!(
thread.entries().len() >= 2,
"Expected at least 2 entries. Got: {:?}",
thread.entries()
);
assert!(matches!(
thread.entries()[0],
AgentThreadEntry::UserMessage(_)
@@ -79,37 +86,44 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
.unwrap();
thread.read_with(cx, |thread, cx| {
assert_eq!(thread.entries().len(), 3);
assert!(matches!(
thread.entries()[0],
AgentThreadEntry::UserMessage(_)
));
assert!(matches!(thread.entries()[1], AgentThreadEntry::ToolCall(_)));
let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries()[2] else {
panic!("Expected AssistantMessage")
};
let assistant_message = &thread
.entries()
.iter()
.rev()
.find_map(|entry| match entry {
AgentThreadEntry::AssistantMessage(msg) => Some(msg),
_ => None,
})
.unwrap();
assert!(
assistant_message.to_markdown(cx).contains("Hello, world!"),
"unexpected assistant message: {:?}",
assistant_message.to_markdown(cx)
);
});
drop(tempdir);
}
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
fs.insert_tree(
path!("/private/tmp"),
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
)
.await;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
let _fs = init_test(cx).await;
let tempdir = tempfile::tempdir().unwrap();
let foo_path = tempdir.path().join("foo");
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
thread
.update(cx, |thread, cx| {
thread.send_raw(
"Read the '/private/tmp/foo' file and tell me what you see.",
&format!("Read {} and tell me what you see.", foo_path.display()),
cx,
)
})
@@ -132,10 +146,13 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
);
});
drop(tempdir);
}
pub async fn test_tool_call_with_confirmation(
server: impl AgentServer + 'static,
allow_option_id: acp::PermissionOptionId,
cx: &mut TestAppContext,
) {
let fs = init_test(cx).await;
@@ -143,7 +160,7 @@ pub async fn test_tool_call_with_confirmation(
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#,
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
cx,
)
});
@@ -163,10 +180,10 @@ pub async fn test_tool_call_with_confirmation(
)
.await;
let tool_call_id = thread.read_with(cx, |thread, _cx| {
let tool_call_id = thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
content,
label,
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread
@@ -178,7 +195,8 @@ pub async fn test_tool_call_with_confirmation(
panic!();
};
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
let label = label.read(cx).source();
assert!(label.contains("touch"), "Got: {}", label);
id.clone()
});
@@ -186,7 +204,7 @@ pub async fn test_tool_call_with_confirmation(
thread.update(cx, |thread, cx| {
thread.authorize_tool_call(
tool_call_id,
acp::PermissionOptionId("0".into()),
allow_option_id,
acp::PermissionOptionKind::AllowOnce,
cx,
);
@@ -230,7 +248,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#,
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
cx,
)
});
@@ -250,10 +268,10 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
)
.await;
thread.read_with(cx, |thread, _cx| {
thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
content,
label,
status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread.entries()[first_tool_call_ix]
@@ -261,7 +279,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
panic!("{:?}", thread.entries()[1]);
};
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
let label = label.read(cx).source();
assert!(label.contains("touch"), "Got: {}", label);
id.clone()
});
@@ -294,7 +313,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
#[macro_export]
macro_rules! common_e2e_tests {
($server:expr) => {
($server:expr, allow_option_id = $allow_option_id:expr) => {
mod common_e2e {
use super::*;
@@ -319,7 +338,12 @@ macro_rules! common_e2e_tests {
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn tool_call_with_confirmation(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_tool_call_with_confirmation($server, cx).await;
$crate::e2e_tests::test_tool_call_with_confirmation(
$server,
::agent_client_protocol::PermissionOptionId($allow_option_id.into()),
cx,
)
.await;
}
#[::gpui::test]
@@ -351,6 +375,9 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
gemini: Some(AgentServerSettings {
command: crate::gemini::tests::local_command(),
}),
codex: Some(AgentServerSettings {
command: crate::codex::tests::local_command(),
}),
},
cx,
);
@@ -409,3 +436,24 @@ pub async fn run_until_first_tool_call(
}
}
}
pub fn get_zed_path() -> PathBuf {
let mut zed_path = std::env::current_exe().unwrap();
while zed_path
.file_name()
.map_or(true, |name| name.to_string_lossy() != "debug")
{
if !zed_path.pop() {
panic!("Could not find target directory");
}
}
zed_path.push("zed");
if !zed_path.exists() {
panic!("\n🚨 Run `cargo build` at least once before running e2e tests\n\n");
}
zed_path
}

View File

@@ -107,6 +107,7 @@ impl AgentServer for Gemini {
name,
connection,
child_status,
current_thread: thread_rc,
});
Ok(connection)
@@ -188,7 +189,7 @@ pub(crate) mod tests {
use crate::AgentServerCommand;
use std::path::Path;
crate::common_e2e_tests!(Gemini);
crate::common_e2e_tests!(Gemini, allow_option_id = "0");
pub fn local_command() -> AgentServerCommand {
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))

View File

@@ -0,0 +1,207 @@
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use anyhow::Result;
use context_server::listener::{McpServerTool, ToolResponse};
use context_server::types::{
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
ToolsCapabilities, requests,
};
use futures::channel::oneshot;
use gpui::{App, AsyncApp, Task, WeakEntity};
use indoc::indoc;
pub struct ZedMcpServer {
server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
impl ZedMcpServer {
pub async fn new(
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
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(RequestPermissionTool {
thread_rx: thread_rx.clone(),
});
mcp_server.add_tool(ReadTextFileTool {
thread_rx: thread_rx.clone(),
});
mcp_server.add_tool(WriteTextFileTool {
thread_rx: thread_rx.clone(),
});
Ok(Self { server: mcp_server })
}
pub fn server_config(&self) -> Result<acp::McpServerConfig> {
#[cfg(not(test))]
let zed_path = anyhow::Context::context(
std::env::current_exe(),
"finding current executable path for use in mcp_server",
)?;
#[cfg(test)]
let zed_path = crate::e2e_tests::get_zed_path();
Ok(acp::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,
})
})
}
}
// Tools
#[derive(Clone)]
pub struct RequestPermissionTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for RequestPermissionTool {
type Input = acp::RequestPermissionArguments;
type Output = acp::RequestPermissionOutput;
const NAME: &'static str = "Confirmation";
fn description(&self) -> &'static str {
indoc! {"
Request permission for tool calls.
This tool is meant to be called programmatically by the agent loop, not the LLM.
"}
}
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 result = thread
.update(cx, |thread, cx| {
thread.request_tool_call_permission(input.tool_call, input.options, cx)
})?
.await;
let outcome = match result {
Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
};
Ok(ToolResponse {
content: vec![],
structured_content: acp::RequestPermissionOutput { outcome },
})
}
}
#[derive(Clone)]
pub struct ReadTextFileTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for ReadTextFileTool {
type Input = acp::ReadTextFileArguments;
type Output = acp::ReadTextFileOutput;
const NAME: &'static str = "Read";
fn description(&self) -> &'static str {
"Reads the content of the given file in the project including unsaved changes."
}
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.path, input.line, input.limit, false, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![],
structured_content: acp::ReadTextFileOutput { content },
})
}
}
#[derive(Clone)]
pub struct WriteTextFileTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl McpServerTool for WriteTextFileTool {
type Input = acp::WriteTextFileArguments;
type Output = ();
const NAME: &'static str = "Write";
fn description(&self) -> &'static str {
"Write to a file replacing its contents"
}
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.path, input.content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![],
structured_content: (),
})
}
}

View File

@@ -13,6 +13,7 @@ pub fn init(cx: &mut App) {
pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>,
pub codex: Option<AgentServerSettings>,
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
@@ -29,13 +30,21 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
for AllAgentServersSettings {
gemini,
claude,
codex,
} in sources.defaults_and_customizations()
{
if gemini.is_some() {
settings.gemini = gemini.clone();
}
if claude.is_some() {
settings.claude = claude.clone();
}
if codex.is_some() {
settings.codex = codex.clone();
}
}
Ok(settings)

View File

@@ -13,6 +13,7 @@ path = "src/agent_settings.rs"
[dependencies]
anyhow.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
gpui.workspace = true
language_model.workspace = true
@@ -20,7 +21,6 @@ schemars.workspace = true
serde.workspace = true
settings.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
fs.workspace = true

View File

@@ -321,11 +321,11 @@ pub enum CompletionMode {
Burn,
}
impl From<CompletionMode> for zed_llm_client::CompletionMode {
impl From<CompletionMode> for cloud_llm_client::CompletionMode {
fn from(value: CompletionMode) -> Self {
match value {
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
}
}
}

View File

@@ -31,6 +31,7 @@ audio.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
@@ -46,9 +47,9 @@ futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
html_to_markdown.workspace = true
indoc.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
indoc.workspace = true
inventory.workspace = true
itertools.workspace = true
jsonschema.workspace = true
@@ -97,7 +98,6 @@ watch.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
assistant_tools.workspace = true

View File

@@ -1,5 +1,7 @@
use acp_thread::{AgentConnection, Plan};
use agent_servers::AgentServer;
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use audio::{Audio, Sound};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
@@ -18,10 +20,10 @@ use editor::{
use file_icons::FileIcons;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
pulsating_between,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
list, percentage, point, prelude::*, pulsating_between,
};
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
@@ -45,7 +47,10 @@ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSe
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
use crate::ui::{AgentNotification, AgentNotificationEvent};
use crate::{
AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
};
const RESPONSE_PADDING_X: Pixels = px(19.);
@@ -59,6 +64,8 @@ pub struct AcpThreadView {
message_set_from_history: bool,
_message_editor_subscription: Subscription,
mention_set: Arc<Mutex<MentionSet>>,
notifications: Vec<WindowHandle<AgentNotification>>,
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
last_error: Option<Entity<Markdown>>,
list_state: ListState,
auth_task: Option<Task<()>>,
@@ -174,6 +181,8 @@ impl AcpThreadView {
message_set_from_history: false,
_message_editor_subscription: message_editor_subscription,
mention_set,
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
diff_editors: Default::default(),
list_state: list_state,
last_error: None,
@@ -381,7 +390,9 @@ impl AcpThreadView {
return;
}
let Some(thread) = self.thread() else { return };
let Some(thread) = self.thread() else {
return;
};
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
cx.spawn(async move |this, cx| {
@@ -564,6 +575,30 @@ impl AcpThreadView {
self.sync_thread_entry_view(index, window, cx);
self.list_state.splice(index..index + 1, 1);
}
AcpThreadEvent::ToolAuthorizationRequired => {
self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
}
AcpThreadEvent::Stopped => {
let used_tools = thread.read(cx).used_tools_since_last_user_message();
self.notify_with_sound(
if used_tools {
"Finished running tools"
} else {
"New message"
},
IconName::ZedAssistant,
window,
cx,
);
}
AcpThreadEvent::Error => {
self.notify_with_sound(
"Agent stopped due to an error",
IconName::Warning,
window,
cx,
);
}
}
cx.notify();
}
@@ -872,7 +907,10 @@ impl AcpThreadView {
let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
let status_icon = match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { .. } => None,
ToolCallStatus::Allowed {
status: acp::ToolCallStatus::Pending,
}
| ToolCallStatus::WaitingForConfirmation { .. } => None,
ToolCallStatus::Allowed {
status: acp::ToolCallStatus::InProgress,
..
@@ -957,6 +995,8 @@ impl AcpThreadView {
Icon::new(match tool_call.kind {
acp::ToolKind::Read => IconName::ToolRead,
acp::ToolKind::Edit => IconName::ToolPencil,
acp::ToolKind::Delete => IconName::ToolDeleteFile,
acp::ToolKind::Move => IconName::ArrowRightLeft,
acp::ToolKind::Search => IconName::ToolSearch,
acp::ToolKind::Execute => IconName::ToolTerminal,
acp::ToolKind::Think => IconName::ToolBulb,
@@ -1068,6 +1108,7 @@ impl AcpThreadView {
options,
entry_ix,
tool_call.id.clone(),
tool_call.content.is_empty(),
cx,
)),
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
@@ -1126,6 +1167,7 @@ impl AcpThreadView {
options: &[acp::PermissionOption],
entry_ix: usize,
tool_call_id: acp::ToolCallId,
empty_content: bool,
cx: &Context<Self>,
) -> Div {
h_flex()
@@ -1133,8 +1175,10 @@ impl AcpThreadView {
.px_1p5()
.gap_1()
.justify_end()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.when(!empty_content, |this| {
this.border_t_1()
.border_color(self.tool_card_border_color(cx))
})
.children(options.iter().map(|option| {
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.label.clone())
@@ -2151,6 +2195,154 @@ impl AcpThreadView {
self.list_state.scroll_to(ListOffset::default());
cx.notify();
}
fn notify_with_sound(
&mut self,
caption: impl Into<SharedString>,
icon: IconName,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.play_notification_sound(window, cx);
self.show_notification(caption, icon, window, cx);
}
fn play_notification_sound(&self, window: &Window, cx: &mut App) {
let settings = AgentSettings::get_global(cx);
if settings.play_sound_when_agent_done && !window.is_window_active() {
Audio::play_sound(Sound::AgentDone, cx);
}
}
fn show_notification(
&mut self,
caption: impl Into<SharedString>,
icon: IconName,
window: &mut Window,
cx: &mut Context<Self>,
) {
if window.is_window_active() || !self.notifications.is_empty() {
return;
}
let title = self.title(cx);
match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title, window, primary, cx);
}
}
NotifyWhenAgentWaiting::AllScreens => {
let caption = caption.into();
for screen in cx.displays() {
self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
}
}
NotifyWhenAgentWaiting::Never => {
// Don't show anything
}
}
}
fn pop_up(
&mut self,
icon: IconName,
caption: SharedString,
title: SharedString,
window: &mut Window,
screen: Rc<dyn PlatformDisplay>,
cx: &mut Context<Self>,
) {
let options = AgentNotification::window_options(screen, cx);
let project_name = self.workspace.upgrade().and_then(|workspace| {
workspace
.read(cx)
.project()
.read(cx)
.visible_worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).root_name().to_string())
});
if let Some(screen_window) = cx
.open_window(options, |_, cx| {
cx.new(|_| {
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
})
})
.log_err()
{
if let Some(pop_up) = screen_window.entity(cx).log_err() {
self.notification_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
AgentNotificationEvent::Accepted => {
let handle = window.window_handle();
cx.activate(true);
let workspace_handle = this.workspace.clone();
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| {
handle
.update(cx, |_view, window, _cx| {
window.activate_window();
if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(_cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
});
}
})
.log_err();
});
this.dismiss_notifications(cx);
}
AgentNotificationEvent::Dismissed => {
this.dismiss_notifications(cx);
}
}
}));
self.notifications.push(screen_window);
// If the user manually refocuses the original window, dismiss the popup.
self.notification_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push({
let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |_, window, cx| {
if window.is_window_active() {
if let Some(pop_up) = pop_up_weak.upgrade() {
pop_up.update(cx, |_, cx| {
cx.emit(AgentNotificationEvent::Dismissed);
});
}
}
})
});
}
}
}
fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
for window in self.notifications.drain(..) {
window
.update(cx, |_, window, _| {
window.remove_window();
})
.ok();
self.notification_subscriptions.remove(&window);
}
}
}
impl Focusable for AcpThreadView {
@@ -2432,3 +2624,331 @@ fn plan_label_markdown_style(
..default_md_style
}
}
#[cfg(test)]
mod tests {
use agent_client_protocol::SessionId;
use editor::EditorSettings;
use fs::FakeFs;
use futures::future::try_join_all;
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use rand::Rng;
use settings::SettingsStore;
use super::*;
#[gpui::test]
async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some())
);
}
#[gpui::test]
async fn test_notification_for_error(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) =
setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some())
);
}
#[gpui::test]
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
let tool_call_id = acp::ToolCallId("1".into());
let tool_call = acp::ToolCall {
id: tool_call_id.clone(),
label: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Pending,
content: vec!["hi".into()],
locations: vec![],
raw_input: None,
};
let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
.with_permission_requests(HashMap::from_iter([(
tool_call_id,
vec![acp::PermissionOption {
id: acp::PermissionOptionId("1".into()),
label: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
}],
)]));
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some())
);
}
async fn setup_thread_view(
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,
) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let thread_view = cx.update(|window, cx| {
cx.new(|cx| {
AcpThreadView::new(
Rc::new(agent),
workspace.downgrade(),
project,
Rc::new(RefCell::new(MessageHistory::default())),
1,
None,
window,
cx,
)
})
});
cx.run_until_parked();
(thread_view, cx)
}
struct StubAgentServer<C> {
connection: C,
}
impl<C> StubAgentServer<C> {
fn new(connection: C) -> Self {
Self { connection }
}
}
impl StubAgentServer<StubAgentConnection> {
fn default() -> Self {
Self::new(StubAgentConnection::default())
}
}
impl<C> AgentServer for StubAgentServer<C>
where
C: 'static + AgentConnection + Send + Clone,
{
fn logo(&self) -> ui::IconName {
unimplemented!()
}
fn name(&self) -> &'static str {
unimplemented!()
}
fn empty_state_headline(&self) -> &'static str {
unimplemented!()
}
fn empty_state_message(&self) -> &'static str {
unimplemented!()
}
fn connect(
&self,
_root_dir: &Path,
_project: &Entity<Project>,
_cx: &mut App,
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
Task::ready(Ok(Rc::new(self.connection.clone())))
}
}
#[derive(Clone, Default)]
struct StubAgentConnection {
sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
updates: Vec<acp::SessionUpdate>,
}
impl StubAgentConnection {
fn new(updates: Vec<acp::SessionUpdate>) -> Self {
Self {
updates,
permission_requests: HashMap::default(),
sessions: Arc::default(),
}
}
fn with_permission_requests(
mut self,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
) -> Self {
self.permission_requests = permission_requests;
self
}
}
impl AgentConnection for StubAgentConnection {
fn name(&self) -> &'static str {
"StubAgentConnection"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::AsyncApp,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = SessionId(
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>()
.into(),
);
let thread = cx
.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))
.unwrap();
self.sessions.lock().insert(session_id, thread.downgrade());
Task::ready(Ok(thread))
}
fn authenticate(&self, _cx: &mut App) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<gpui::Result<()>> {
let sessions = self.sessions.lock();
let thread = sessions.get(&params.session_id).unwrap();
let mut tasks = vec![];
for update in &self.updates {
let thread = thread.clone();
let update = update.clone();
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
&& let Some(options) = self.permission_requests.get(&tool_call.id)
{
Some((tool_call.clone(), options.clone()))
} else {
None
};
let task = cx.spawn(async move |cx| {
if let Some((tool_call, options)) = permission_request {
let permission = thread.update(cx, |thread, cx| {
thread.request_tool_call_permission(
tool_call.clone(),
options.clone(),
cx,
)
})?;
permission.await?;
}
thread.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
})?;
anyhow::Ok(())
});
tasks.push(task);
}
cx.spawn(async move |_| {
try_join_all(tasks).await?;
Ok(())
})
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
unimplemented!()
}
}
#[derive(Clone)]
struct SaboteurAgentConnection;
impl AgentConnection for SaboteurAgentConnection {
fn name(&self) -> &'static str {
"SaboteurAgentConnection"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::AsyncApp,
) -> Task<gpui::Result<Entity<AcpThread>>> {
Task::ready(Ok(cx
.new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx))
.unwrap()))
}
fn authenticate(&self, _cx: &mut App) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task<gpui::Result<()>> {
Task::ready(Err(anyhow::anyhow!("Error prompting")))
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
unimplemented!()
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});
}
}

View File

@@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use anyhow::Context as _;
use assistant_tool::ToolUseStatus;
use audio::{Audio, Sound};
use cloud_llm_client::CompletionIntent;
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::scroll::Autoscroll;
@@ -52,7 +53,6 @@ use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
use workspace::{CollaboratorId, Workspace};
use zed_actions::assistant::OpenRulesLibrary;
use zed_llm_client::CompletionIntent;
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;

View File

@@ -7,6 +7,7 @@ use std::{sync::Arc, time::Duration};
use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
use extension::ExtensionManifest;
@@ -25,7 +26,6 @@ use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
use proto::Plan;
use settings::{Settings, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
@@ -180,11 +180,18 @@ impl AgentConfiguration {
let current_plan = if is_zed_provider {
self.workspace
.upgrade()
.and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
.and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
} else {
None
};
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected()
})
.unwrap_or(false);
v_flex()
.w_full()
.when(is_expanded, |this| this.mb_2())
@@ -233,8 +240,8 @@ impl AgentConfiguration {
.size(LabelSize::Large),
)
.map(|this| {
if is_zed_provider {
this.gap_2().child(
if is_zed_provider && is_signed_in {
this.child(
self.render_zed_plan_info(current_plan, cx),
)
} else {
@@ -397,9 +404,11 @@ impl AgentConfiguration {
let fs = self.fs.clone();
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
"Agent edits are also displayed in single-file editors for review.",
"always-allow-tool-actions-switch",
"Allow running commands without asking for confirmation",
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -417,7 +426,7 @@ impl AgentConfiguration {
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
"Agent edits are also displayed in single-file editors for review.",
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -435,7 +444,9 @@ impl AgentConfiguration {
SwitchField::new(
"sound-notification",
"Play sound when finished generating",
"Hear a notification sound when the agent is done generating changes or needs your input.",
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -453,7 +464,9 @@ impl AgentConfiguration {
SwitchField::new(
"modifier-send",
"Use modifier to submit a message",
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@@ -495,7 +508,7 @@ impl AgentConfiguration {
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match plan {
Plan::Free => ("Free", Color::Default, free_chip_bg),
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
};

View File

@@ -30,7 +30,7 @@ use std::{
sync::Arc,
time::Duration,
};
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use ui::{ButtonSize, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
@@ -791,22 +791,32 @@ fn render_diff_hunk_controls(
) -> AnyElement {
let editor = editor.clone();
h_flex()
// Get controls positioning from editor state
let controls_above = editor.read(cx).diff_hunk_controls_above();
let mut container = h_flex()
.h(line_height)
.mr_0p5()
.gap_1()
.px_0p5()
.pb_1()
.py_0p5()
.border_x_1()
.border_b_1()
.border_color(cx.theme().colors().border)
.rounded_b_md()
.bg(cx.theme().colors().editor_background)
.gap_1()
.block_mouse_except_scroll()
.shadow_md()
.shadow_md();
if controls_above {
container = container.border_t_1().rounded_t_md();
} else {
container = container.border_b_1().rounded_b_md();
}
container
.children(vec![
Button::new(("reject", row as u64), "Reject")
.size(ButtonSize::Compact)
.disabled(is_created_file)
.key_binding(
KeyBinding::for_action_in(
@@ -835,6 +845,7 @@ fn render_diff_hunk_controls(
}
}),
Button::new(("keep", row as u64), "Keep")
.size(ButtonSize::Compact)
.key_binding(
KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx)
.map(|kb| kb.size(rems_from_px(12.))),
@@ -1521,6 +1532,9 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx);
}
}
AcpThreadEvent::Stopped
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::Error => {}
}
}

View File

@@ -44,6 +44,7 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
use assistant_slash_command::SlashCommandWorkingSet;
use assistant_tool::ToolWorkingSet;
use client::{DisableAiSettings, UserStore, zed_urls};
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs;
@@ -59,7 +60,6 @@ use language_model::{
};
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
@@ -77,10 +77,9 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
assistant::{OpenRulesLibrary, ToggleFocus},
};
use zed_llm_client::{CompletionIntent, UsageLimit};
const AGENT_PANEL_KEY: &str = "agent_panel";
@@ -105,7 +104,7 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
.register_action(|workspace, _: &OpenSettings, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
@@ -564,22 +563,8 @@ impl AgentPanel {
let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let message_editor = cx.new(|cx| {
MessageEditor::new(
fs.clone(),
workspace.clone(),
user_store.clone(),
message_editor_context_store.clone(),
prompt_store.clone(),
thread_store.downgrade(),
context_store.downgrade(),
thread.clone(),
window,
cx,
)
});
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
@@ -589,6 +574,21 @@ impl AgentPanel {
)
});
let message_editor = cx.new(|cx| {
MessageEditor::new(
fs.clone(),
workspace.clone(),
message_editor_context_store.clone(),
prompt_store.clone(),
thread_store.downgrade(),
context_store.downgrade(),
Some(history_store.downgrade()),
thread.clone(),
window,
cx,
)
});
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_thread = cx.new(|cx| {
@@ -846,11 +846,11 @@ impl AgentPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.user_store.clone(),
context_store.clone(),
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(),
window,
cx,
@@ -1119,11 +1119,11 @@ impl AgentPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.user_store.clone(),
context_store,
self.prompt_store.clone(),
self.thread_store.downgrade(),
self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(),
window,
cx,
@@ -1987,6 +1987,20 @@ impl AgentPanel {
);
}),
)
.item(
ContextMenuEntry::new("New Codex Thread")
.icon(IconName::AiOpenAi)
.icon_color(Color::Muted)
.handler(move |window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Codex),
}
.boxed_clone(),
cx,
);
}),
)
});
menu
}))
@@ -2012,65 +2026,69 @@ impl AgentPanel {
)
.anchor(Corner::TopRight)
.with_handle(self.agent_panel_menu_handle.clone())
.menu(move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
if let Some(usage) = usage {
.menu({
let focus_handle = focus_handle.clone();
move |window, cx| {
Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
menu = menu.context(focus_handle.clone());
if let Some(usage) = usage {
menu = menu
.header_with_link("Prompt Usage", "Manage", account_url.clone())
.custom_entry(
move |_window, cx| {
let used_percentage = match usage.limit {
UsageLimit::Limited(limit) => {
Some((usage.amount as f32 / limit as f32) * 100.)
}
UsageLimit::Unlimited => None,
};
h_flex()
.flex_1()
.gap_1p5()
.children(used_percentage.map(|percent| {
ProgressBar::new("usage", percent, 100., cx)
}))
.child(
Label::new(match usage.limit {
UsageLimit::Limited(limit) => {
format!("{} / {limit}", usage.amount)
}
UsageLimit::Unlimited => {
format!("{} / ∞", usage.amount)
}
})
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
)
.separator()
}
menu = menu
.header_with_link("Prompt Usage", "Manage", account_url.clone())
.custom_entry(
move |_window, cx| {
let used_percentage = match usage.limit {
UsageLimit::Limited(limit) => {
Some((usage.amount as f32 / limit as f32) * 100.)
}
UsageLimit::Unlimited => None,
};
h_flex()
.flex_1()
.gap_1p5()
.children(used_percentage.map(|percent| {
ProgressBar::new("usage", percent, 100., cx)
}))
.child(
Label::new(match usage.limit {
UsageLimit::Limited(limit) => {
format!("{} / {limit}", usage.amount)
}
UsageLimit::Unlimited => {
format!("{} / ∞", usage.amount)
}
})
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
},
move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
.header("MCP Servers")
.action(
"View Server Extensions",
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
id: None,
}),
)
.separator()
}
.action("Add Custom Server…", Box::new(AddContextServer))
.separator();
menu = menu
.header("MCP Servers")
.action(
"View Server Extensions",
Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::ContextServers,
),
id: None,
}),
)
.action("Add Custom Server…", Box::new(AddContextServer))
.separator();
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
.action(zoom_in_label, Box::new(ToggleZoom));
menu
}))
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenSettings))
.action(zoom_in_label, Box::new(ToggleZoom));
menu
}))
}
});
h_flex()
@@ -2271,10 +2289,10 @@ impl AgentPanel {
| ActiveView::Configuration => return false,
}
let plan = self.user_store.read(cx).current_plan();
let plan = self.user_store.read(cx).plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
}
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
@@ -2283,20 +2301,21 @@ impl AgentPanel {
}
match &self.active_view {
ActiveView::Thread { thread, .. } => thread
.read(cx)
.thread()
.read(cx)
.configured_model()
.map_or(true, |model| {
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
}),
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx)
.read(cx)
.default_model()
.map_or(true, |model| {
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
}),
ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
let history_is_empty = self
.history_store
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.any(|provider| {
provider.is_authenticated(cx)
&& provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
});
history_is_empty || !has_configured_non_zed_providers
}
ActiveView::ExternalAgentThread { .. }
| ActiveView::History
| ActiveView::Configuration => false,
@@ -2317,9 +2336,8 @@ impl AgentPanel {
Some(
div()
.size_full()
.when(thread_view, |this| {
this.bg(cx.theme().colors().panel_background)
this.size_full().bg(cx.theme().colors().panel_background)
})
.when(text_thread_view, |this| {
this.bg(cx.theme().colors().editor_background)
@@ -2460,14 +2478,14 @@ impl AgentPanel {
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&OpenSettings,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
OpenSettings.boxed_clone(),
cx,
)
}),
@@ -2644,6 +2662,25 @@ impl AgentPanel {
)
},
),
)
.child(
NewThreadButton::new(
"new-codex-thread-btn",
"New Codex Thread",
IconName::AiOpenAi,
)
.on_click(
|window, cx| {
window.dispatch_action(
Box::new(NewExternalAgentThread {
agent: Some(
crate::ExternalAgent::Codex,
),
}),
cx,
)
},
),
),
)
}),
@@ -2672,16 +2709,11 @@ impl AgentPanel {
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
window.dispatch_action(OpenSettings.boxed_clone(), cx)
}),
),
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
@@ -2875,7 +2907,7 @@ impl AgentPanel {
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
};
let icon = Icon::new(IconName::XCircle)
@@ -3185,7 +3217,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx);
}))
.on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
.on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
this.open_configuration(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))

View File

@@ -150,6 +150,7 @@ enum ExternalAgent {
#[default]
Gemini,
ClaudeCode,
Codex,
}
impl ExternalAgent {
@@ -157,6 +158,7 @@ impl ExternalAgent {
match self {
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
ExternalAgent::Codex => Rc::new(agent_servers::Codex),
}
}
}
@@ -262,6 +264,8 @@ fn update_command_palette_filter(cx: &mut App) {
if disable_ai {
filter.hide_namespace("agent");
filter.hide_namespace("assistant");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.hide_namespace("zed_predict_onboarding");
filter.hide_namespace("edit_prediction");
@@ -282,6 +286,7 @@ fn update_command_palette_filter(cx: &mut App) {
} else {
filter.show_namespace("agent");
filter.show_namespace("assistant");
filter.show_namespace("copilot");
filter.show_namespace("zed_predict_onboarding");
filter.show_namespace("edit_prediction");

View File

@@ -6,6 +6,7 @@ use agent::{
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use cloud_llm_client::CompletionIntent;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{
@@ -35,7 +36,6 @@ use std::{
};
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use zed_llm_client::CompletionIntent;
pub struct BufferCodegen {
alternatives: Vec<Entity<CodegenAlternative>>,

View File

@@ -1,10 +1,10 @@
#![allow(unused, dead_code)]
use client::{ModelRequestUsage, RequestUsage};
use cloud_llm_client::{Plan, UsageLimit};
use gpui::Global;
use std::ops::{Deref, DerefMut};
use ui::prelude::*;
use zed_llm_client::{Plan, UsageLimit};
/// Debug only: Used for testing various account states
///

View File

@@ -48,7 +48,7 @@ use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::{RangeExt, ResultExt, maybe};
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
use zed_actions::agent::OpenSettings;
pub fn init(
fs: Arc<dyn Fs>,
@@ -345,7 +345,7 @@ impl InlineAssistant {
if let Some(answer) = answer {
if answer == 0 {
cx.update(|window, cx| {
window.dispatch_action(Box::new(OpenConfiguration), cx)
window.dispatch_action(Box::new(OpenSettings), cx)
})
.ok();
}

View File

@@ -576,7 +576,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenConfiguration.boxed_clone(),
zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
}),

View File

@@ -9,6 +9,7 @@ use crate::ui::{
MaxModeTooltip,
preview::{AgentPreview, UsageCallout},
};
use agent::history_store::HistoryStore;
use agent::{
context::{AgentContextKey, ContextLoadResult, load_context},
context_store::ContextStoreEvent,
@@ -16,7 +17,7 @@ use agent::{
use agent_settings::{AgentSettings, CompletionMode};
use ai_onboarding::ApiKeysWithProviders;
use buffer_diff::BufferDiff;
use client::UserStore;
use cloud_llm_client::CompletionIntent;
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::display_map::CreaseId;
@@ -29,8 +30,9 @@ use fs::Fs;
use futures::future::Shared;
use futures::{FutureExt as _, future};
use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task,
TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
pulsating_between,
};
use language::{Buffer, Language, Point};
use language_model::{
@@ -40,7 +42,6 @@ use language_model::{
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
@@ -51,7 +52,6 @@ use util::ResultExt as _;
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
use zed_actions::agent::ToggleModelSelector;
use zed_llm_client::CompletionIntent;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
@@ -77,9 +77,9 @@ pub struct MessageEditor {
editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
history_store: Option<WeakEntity<HistoryStore>>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>,
@@ -156,11 +156,11 @@ impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>,
history_store: Option<WeakEntity<HistoryStore>>,
thread: Entity<Thread>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -227,12 +227,12 @@ impl MessageEditor {
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
user_store,
thread,
incompatible_tools_state: incompatible_tools.clone(),
workspace,
context_store,
prompt_store,
history_store,
context_strip,
context_picker_menu_handle,
load_context_task: None,
@@ -1282,24 +1282,12 @@ impl MessageEditor {
return None;
}
let user_store = self.user_store.read(cx);
let ubb_enable = user_store
.usage_based_billing_enabled()
.map_or(false, |enabled| enabled);
if ubb_enable {
let user_store = self.project.read(cx).user_store().read(cx);
if user_store.is_usage_based_billing_enabled() {
return None;
}
let plan = user_store
.current_plan()
.map(|plan| match plan {
Plan::Free => zed_llm_client::Plan::ZedFree,
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
})
.unwrap_or(zed_llm_client::Plan::ZedFree);
let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
let usage = user_store.model_request_usage()?;
@@ -1661,32 +1649,36 @@ impl Render for MessageEditor {
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
let in_pro_trial = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedProTrial)
);
let has_configured_providers = LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.count()
> 0;
let pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
let is_signed_out = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(true);
let configured_providers: Vec<(IconName, SharedString)> =
LanguageModelRegistry::read_global(cx)
.providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0.clone()))
.collect();
let has_existing_providers = configured_providers.len() > 0;
let has_history = self
.history_store
.as_ref()
.and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
.unwrap_or(false)
|| self
.thread
.read_with(cx, |thread, _| thread.messages().len() > 0);
v_flex()
.size_full()
.bg(cx.theme().colors().panel_background)
.when(
has_existing_providers && !in_pro_trial && !pro_user,
!has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)),
)
.when(changed_buffers.len() > 0, |parent| {
@@ -1760,7 +1752,6 @@ impl AgentPreview for MessageEditor {
) -> Option<AnyElement> {
if let Some(workspace) = workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
let user_store = workspace.read(cx).app_state().user_store.clone();
let project = workspace.read(cx).project().clone();
let weak_project = project.downgrade();
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
@@ -1773,11 +1764,11 @@ impl AgentPreview for MessageEditor {
MessageEditor::new(
fs,
workspace.downgrade(),
user_store,
context_store,
None,
thread_store.downgrade(),
text_thread_store.downgrade(),
None,
thread,
window,
cx,

View File

@@ -10,6 +10,7 @@ use agent::{
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use cloud_llm_client::CompletionIntent;
use collections::{HashMap, VecDeque};
use editor::{MultiBuffer, actions::SelectAll};
use fs::Fs;
@@ -27,7 +28,6 @@ use terminal_view::TerminalView;
use ui::prelude::*;
use util::ResultExt;
use workspace::{Toast, Workspace, notifications::NotificationId};
use zed_llm_client::CompletionIntent;
pub fn init(
fs: Arc<dyn Fs>,

View File

@@ -5,7 +5,6 @@ mod end_trial_upsell;
mod new_thread_button;
mod onboarding_modal;
pub mod preview;
mod upsell;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
use client::zed_urls;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, prelude::*};
use ui::{Divider, List, Tooltip, prelude::*};
#[derive(IntoElement, RegisterComponent)]
pub struct EndTrialUpsell {
@@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
)
.child(
List::new()
.child(BulletItem::new("500 prompts per month with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")),
.child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
)
.child(
Button::new("cta-button", "Upgrade to Zed Pro")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))),
.on_click(move |_, _window, cx| {
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
);
let free_section = v_flex()
@@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
.color(Color::Muted)
.buffer_font(cx),
)
.child(
Label::new("(Current Plan)")
.size(LabelSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new(
"50 prompts per month with the Claude models",
))
.child(BulletItem::new(
"2000 accepted edit predictions using our open-source Zeta model",
)),
)
.child(
Button::new("dismiss-button", "Stay on Free")
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| callback(window, cx)
}),
.child(BulletItem::new("50 prompts with the Claude models"))
.child(BulletItem::new("2,000 accepted edit predictions")),
);
AgentPanelOnboardingCard::new()
.child(Headline::new("Your Zed Pro trial has expired."))
.child(Headline::new("Your Zed Pro Trial has expired"))
.child(
Label::new("You've been automatically reset to the Free plan.")
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1(),
.mb_2(),
)
.child(pro_section)
.child(free_section)
.child(
h_flex().absolute().top_4().right_4().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
callback(window, cx)
}
}),
),
)
}
}

View File

@@ -1,8 +1,8 @@
use client::{ModelRequestUsage, RequestUsage, zed_urls};
use cloud_llm_client::{Plan, UsageLimit};
use component::{empty_example, example_group_with_title, single_example};
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Callout, prelude::*};
use zed_llm_client::{Plan, UsageLimit};
#[derive(IntoElement, RegisterComponent)]
pub struct UsageCallout {

View File

@@ -1,163 +0,0 @@
use component::{Component, ComponentScope, single_example};
use gpui::{
AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window,
};
use theme::ActiveTheme;
use ui::{
Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
RegisterComponent, ToggleState, h_flex, v_flex,
};
/// A component that displays an upsell message with a call-to-action button
///
/// # Example
/// ```
/// let upsell = Upsell::new(
/// "Upgrade to Zed Pro",
/// "Get access to advanced AI features and more",
/// "Upgrade Now",
/// Box::new(|_, _window, cx| {
/// cx.open_url("https://zed.dev/pricing");
/// }),
/// Box::new(|_, _window, cx| {
/// // Handle dismiss
/// }),
/// Box::new(|checked, window, cx| {
/// // Handle don't show again
/// }),
/// );
/// ```
#[derive(IntoElement, RegisterComponent)]
pub struct Upsell {
title: SharedString,
message: SharedString,
cta_text: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
}
impl Upsell {
/// Create a new upsell component
pub fn new(
title: impl Into<SharedString>,
message: impl Into<SharedString>,
cta_text: impl Into<SharedString>,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
) -> Self {
Self {
title: title.into(),
message: message.into(),
cta_text: cta_text.into(),
on_click,
on_dismiss,
on_dont_show_again,
}
}
}
impl RenderOnce for Upsell {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.w_full()
.p_4()
.gap_3()
.bg(cx.theme().colors().surface_background)
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_1()
.child(
Label::new(self.title)
.size(ui::LabelSize::Large)
.weight(gpui::FontWeight::BOLD),
)
.child(Label::new(self.message).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.justify_between()
.items_center()
.child(
h_flex()
.items_center()
.gap_1()
.child(
Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
move |_, window, cx| {
(self.on_dont_show_again)(true, window, cx);
},
),
)
.child(
Label::new("Don't show again")
.color(Color::Muted)
.size(ui::LabelSize::Small),
),
)
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "No Thanks")
.style(ButtonStyle::Subtle)
.on_click(self.on_dismiss),
)
.child(
Button::new("cta-button", self.cta_text)
.style(ButtonStyle::Filled)
.on_click(self.on_click),
),
),
)
}
}
impl Component for Upsell {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn name() -> &'static str {
"Upsell"
}
fn description() -> Option<&'static str> {
Some("A promotional component that displays a message with a call-to-action.")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let examples = vec![
single_example(
"Default",
Upsell::new(
"Upgrade to Zed Pro",
"Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
"Upgrade Now",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
single_example(
"Short Message",
Upsell::new(
"Try Zed Pro for free",
"Start your 7-day trial today.",
"Start Trial",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
];
Some(v_flex().gap_4().children(examples).into_any_element())
}
}

View File

@@ -16,10 +16,10 @@ default = []
[dependencies]
client.workspace = true
cloud_llm_client.workspace = true
component.workspace = true
gpui.workspace = true
language_model.workspace = true
proto.workspace = true
serde.workspace = true
smallvec.workspace = true
telemetry.workspace = true

View File

@@ -136,10 +136,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
.full_width()
.style(ButtonStyle::Outlined)
.on_click(move |_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenConfiguration.boxed_clone(),
cx,
);
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
}),
)
}

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
use cloud_llm_client::Plan;
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::prelude::*;
@@ -56,10 +57,8 @@ impl AgentPanelOnboarding {
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let enrolled_in_trial = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedProTrial)
);
let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
AgentPanelOnboardingCard::new()
.child(
@@ -75,7 +74,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
if enrolled_in_trial || self.configured_providers.len() >= 1 {
if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
this
} else {
this.child(ApiKeysWithoutProviders::new())

View File

@@ -1,12 +1,15 @@
mod agent_api_keys_onboarding;
mod agent_panel_onboarding_card;
mod agent_panel_onboarding_content;
mod ai_upsell_card;
mod edit_prediction_onboarding_content;
mod young_account_banner;
pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
pub use ai_upsell_card::AiUpsellCard;
use cloud_llm_client::Plan;
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use young_account_banner::YoungAccountBanner;
@@ -16,6 +19,7 @@ use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
#[derive(IntoElement)]
pub struct BulletItem {
label: SharedString,
}
@@ -28,22 +32,32 @@ impl BulletItem {
}
}
impl IntoElement for BulletItem {
type Element = AnyElement;
impl RenderOnce for BulletItem {
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let line_height = 0.85 * window.line_height();
fn into_element(self) -> Self::Element {
ListItem::new("list-item")
.selectable(false)
.start_slot(
Icon::new(IconName::Dash)
.size(IconSize::XSmall)
.color(Color::Hidden),
.child(
h_flex()
.w_full()
.min_w_0()
.gap_1()
.items_start()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(IconName::Dash)
.size(IconSize::XSmall)
.color(Color::Hidden),
),
)
.child(div().w_full().min_w_0().child(Label::new(self.label))),
)
.child(div().w_full().child(Label::new(self.label)))
.into_any_element()
}
}
#[derive(PartialEq)]
pub enum SignInStatus {
SignedIn,
SigningIn,
@@ -66,7 +80,7 @@ impl From<client::Status> for SignInStatus {
pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus,
pub has_accepted_terms_of_service: bool,
pub plan: Option<proto::Plan>,
pub plan: Option<Plan>,
pub account_too_young: bool,
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
@@ -86,8 +100,8 @@ impl ZedAiOnboarding {
Self {
sign_in_status: status.into(),
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
plan: store.current_plan(),
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
plan: store.plan(),
account_too_young: store.account_too_young(),
continue_with_zed_ai,
accept_terms_of_service: Arc::new({
@@ -100,11 +114,9 @@ impl ZedAiOnboarding {
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
async move |cx| {
client.authenticate_and_connect(true, cx).await;
}
async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
.detach();
.detach_and_log_err(cx);
}),
dismiss_onboarding: None,
}
@@ -373,7 +385,9 @@ impl ZedAiOnboarding {
.child(
List::new()
.child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")),
.child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
)
.child(
Button::new("pro", "Continue with Zed Pro")
@@ -396,9 +410,9 @@ impl RenderOnce for ZedAiOnboarding {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
if self.has_accepted_terms_of_service {
match self.plan {
None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
@@ -418,7 +432,7 @@ impl Component for ZedAiOnboarding {
fn onboarding(
sign_in_status: SignInStatus,
has_accepted_terms_of_service: bool,
plan: Option<proto::Plan>,
plan: Option<Plan>,
account_too_young: bool,
) -> AnyElement {
ZedAiOnboarding {
@@ -453,25 +467,15 @@ impl Component for ZedAiOnboarding {
),
single_example(
"Free Plan",
onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
),
single_example(
"Pro Trial",
onboarding(
SignInStatus::SignedIn,
true,
Some(proto::Plan::ZedProTrial),
false,
),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
),
single_example(
"Pro Plan",
onboarding(
SignInStatus::SignedIn,
true,
Some(proto::Plan::ZedPro),
false,
),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
),
])
.into_any_element(),

View File

@@ -0,0 +1,212 @@
use std::sync::Arc;
use client::{Client, zed_urls};
use cloud_llm_client::Plan;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, Vector, VectorName, prelude::*};
use crate::{BulletItem, SignInStatus};
#[derive(IntoElement, RegisterComponent)]
pub struct AiUpsellCard {
pub sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub user_plan: Option<Plan>,
}
impl AiUpsellCard {
pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
let status = *client.status().borrow();
Self {
user_plan,
sign_in_status: status.into(),
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
.detach_and_log_err(cx);
}),
}
}
}
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let pro_section = v_flex()
.flex_grow()
.w_full()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("Pro")
.size(LabelSize::Small)
.color(Color::Accent)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
);
let free_section = v_flex()
.flex_grow()
.w_full()
.gap_1()
.child(
h_flex()
.gap_2()
.child(
Label::new("Free")
.size(LabelSize::Small)
.color(Color::Muted)
.buffer_font(cx),
)
.child(Divider::horizontal()),
)
.child(
List::new()
.child(BulletItem::new("50 prompts with Claude models"))
.child(BulletItem::new("2,000 accepted edit predictions")),
);
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
);
let gradient_bg = div()
.absolute()
.inset_0()
.size_full()
.bg(gpui::linear_gradient(
180.,
gpui::linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.8),
0.,
),
gpui::linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.),
0.8,
),
));
const DESCRIPTION: &str = "Zed offers a complete agentic experience, with robust editing and reviewing features to collaborate with AI.";
let footer_buttons = match self.sign_in_status {
SignInStatus::SignedIn => v_flex()
.items_center()
.gap_1()
.child(
Button::new("sign_in", "Start 14-day Free Pro Trial")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(move |_, _window, cx| {
telemetry::event!("Start Trial Clicked", state = "post-sign-in");
cx.open_url(&zed_urls::start_trial_url(cx))
}),
)
.child(
Label::new("No credit card required")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Button::new("sign_in", "Sign In")
.full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click({
let callback = self.sign_in.clone();
move |_, window, cx| {
telemetry::event!("Start Trial Clicked", state = "pre-sign-in");
callback(window, cx)
}
})
.into_any_element(),
};
v_flex()
.relative()
.p_4()
.pt_3()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_lg()
.overflow_hidden()
.child(grid_bg)
.child(gradient_bg)
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.child(
div()
.max_w_3_4()
.mb_2()
.child(Label::new(DESCRIPTION).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.mt_1p5()
.mb_2p5()
.items_start()
.gap_6()
.child(free_section)
.child(pro_section),
)
.child(footer_buttons)
}
}
impl Component for AiUpsellCard {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn name() -> &'static str {
"AI Upsell Card"
}
fn sort_name() -> &'static str {
"AI Upsell Card"
}
fn description() -> Option<&'static str> {
Some("A card presenting the Zed AI product during user's first-open onboarding flow.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.p_4()
.gap_4()
.children(vec![example_group(vec![
single_example(
"Signed Out State",
AiUpsellCard {
sign_in_status: SignInStatus::SignedOut,
sign_in: Arc::new(|_, _| {}),
user_plan: None,
}
.into_any_element(),
),
single_example(
"Signed In State",
AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
user_plan: None,
}
.into_any_element(),
),
])])
.into_any_element(),
)
}
}

View File

@@ -19,6 +19,7 @@ assistant_slash_commands.workspace = true
chrono.workspace = true
client.workspace = true
clock.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
context_server.workspace = true
fs.workspace = true
@@ -48,7 +49,6 @@ util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
indoc.workspace = true

View File

@@ -11,6 +11,7 @@ use assistant_slash_command::{
use assistant_slash_commands::FileCommandMetadata;
use client::{self, Client, proto, telemetry::Telemetry};
use clock::ReplicaId;
use cloud_llm_client::CompletionIntent;
use collections::{HashMap, HashSet};
use fs::{Fs, RenameOptions};
use futures::{FutureExt, StreamExt, future::Shared};
@@ -46,7 +47,6 @@ use text::{BufferSnapshot, ToPoint};
use ui::IconName;
use util::{ResultExt, TryFutureExt, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionIntent;
pub use crate::context_store::*;

View File

@@ -767,6 +767,11 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(async move |this, cx| {
pub static ZED_STATELESS: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
if *ZED_STATELESS {
return Ok(());
}
fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?;

View File

@@ -216,7 +216,12 @@ pub trait Tool: 'static + Send + Sync {
/// Returns true if the tool needs the users's confirmation
/// before having permission to run.
fn needs_confirmation(&self, input: &serde_json::Value, cx: &App) -> bool;
fn needs_confirmation(
&self,
input: &serde_json::Value,
project: &Entity<Project>,
cx: &App,
) -> bool;
/// Returns true if the tool may perform edits.
fn may_perform_edits(&self) -> bool;

View File

@@ -375,7 +375,12 @@ mod tests {
false
}
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
fn needs_confirmation(
&self,
_input: &serde_json::Value,
_project: &Entity<Project>,
_cx: &App,
) -> bool {
true
}

View File

@@ -21,9 +21,11 @@ assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
component.workspace = true
derive_more.workspace = true
diffy = "0.4.2"
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
@@ -63,8 +65,6 @@ web_search.workspace = true
which.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_llm_client.workspace = true
diffy = "0.4.2"
[dev-dependencies]
lsp = { workspace = true, features = ["test-support"] }

View File

@@ -44,7 +44,7 @@ impl Tool for CopyPathTool {
"copy_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -37,7 +37,7 @@ impl Tool for CreateDirectoryTool {
include_str!("./create_directory_tool/description.md").into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -33,7 +33,7 @@ impl Tool for DeletePathTool {
"delete_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -46,7 +46,7 @@ impl Tool for DiagnosticsTool {
"diagnostics".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -7,6 +7,7 @@ mod streaming_fuzzy_matcher;
use crate::{Template, Templates};
use anyhow::Result;
use assistant_tool::ActionLog;
use cloud_llm_client::CompletionIntent;
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
pub use edit_parser::EditFormat;
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
@@ -29,7 +30,6 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
use streaming_diff::{CharOperation, StreamingDiff};
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
use util::debug_panic;
use zed_llm_client::CompletionIntent;
#[derive(Serialize)]
struct CreateFilePromptTemplate {

View File

@@ -25,6 +25,7 @@ use language::{
};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use paths;
use project::{
Project, ProjectPath,
lsp_store::{FormatTrigger, LspFormatTarget},
@@ -126,8 +127,47 @@ impl Tool for EditFileTool {
"edit_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
fn needs_confirmation(
&self,
input: &serde_json::Value,
project: &Entity<Project>,
cx: &App,
) -> bool {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return false;
}
let Ok(input) = serde_json::from_value::<EditFileToolInput>(input.clone()) else {
// If it's not valid JSON, it's going to error and confirming won't do anything.
return false;
};
// If any path component matches the local settings folder, then this could affect
// the editor in ways beyond the project source, so prompt.
let local_settings_folder = paths::local_settings_folder_relative_path();
let path = Path::new(&input.path);
if path
.components()
.any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{
return true;
}
// It's also possible that the global config dir is configured to be inside the project,
// so check for that edge case too.
if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
return true;
}
}
// Check if path is inside the global config directory
// First check if it's already inside project - if not, try to canonicalize
let project_path = project.read(cx).find_project_path(&input.path, cx);
// If the path is inside the project, and it's not one of the above edge cases,
// then no confirmation is necessary. Otherwise, confirmation is necessary.
project_path.is_none()
}
fn may_perform_edits(&self) -> bool {
@@ -148,7 +188,25 @@ impl Tool for EditFileTool {
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<EditFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Ok(input) => {
let path = Path::new(&input.path);
let mut description = input.display_description.clone();
// Add context about why confirmation may be needed
let local_settings_folder = paths::local_settings_folder_relative_path();
if path
.components()
.any(|c| c.as_os_str() == local_settings_folder.as_os_str())
{
description.push_str(" (local settings)");
} else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
if canonical_path.starts_with(paths::config_dir()) {
description.push_str(" (global settings)");
}
}
description
}
Err(_) => "Editing file".to_string(),
}
}
@@ -1175,19 +1233,20 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use super::*;
use ::fs::Fs;
use client::TelemetrySettings;
use fs::{FakeFs, Fs};
use gpui::{TestAppContext, UpdateGlobal};
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
use std::fs;
use util::path;
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
@@ -1277,7 +1336,7 @@ mod tests {
) -> anyhow::Result<ProjectPath> {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
@@ -1384,6 +1443,21 @@ mod tests {
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
agent_settings::AgentSettings::register(cx);
Project::init_settings(cx);
});
}
fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
cx.update(|cx| {
// Set custom data directory (config will be under data_dir/config)
paths::set_custom_data_dir(data_dir.to_str().unwrap());
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
agent_settings::AgentSettings::register(cx);
Project::init_settings(cx);
});
}
@@ -1392,7 +1466,7 @@ mod tests {
async fn test_format_on_save(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"src": {}})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
@@ -1591,7 +1665,7 @@ mod tests {
async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"src": {}})).await;
// Create a simple file with trailing whitespace
@@ -1723,4 +1797,641 @@ mod tests {
"Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled"
);
}
#[gpui::test]
async fn test_needs_confirmation(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation
let input_with_zed = json!({
"display_description": "Edit settings",
"path": ".zed/settings.json",
"mode": "edit"
});
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_with_zed, &project, cx),
"Path with .zed component should require confirmation"
);
});
// Test 2: Absolute path should require confirmation
let input_absolute = json!({
"display_description": "Edit file",
"path": "/etc/hosts",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_absolute, &project, cx),
"Absolute path should require confirmation"
);
});
// Test 3: Relative path without .zed should not require confirmation
let input_relative = json!({
"display_description": "Edit file",
"path": "root/src/main.rs",
"mode": "edit"
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input_relative, &project, cx),
"Relative path without .zed should not require confirmation"
);
});
// Test 4: Path with .zed in the middle should require confirmation
let input_zed_middle = json!({
"display_description": "Edit settings",
"path": "root/.zed/tasks.json",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_zed_middle, &project, cx),
"Path with .zed in any component should require confirmation"
);
});
// Test 5: When always_allow_tool_actions is enabled, no confirmation needed
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.always_allow_tool_actions = true;
agent_settings::AgentSettings::override_global(settings, cx);
assert!(
!tool.needs_confirmation(&input_with_zed, &project, cx),
"When always_allow_tool_actions is true, no confirmation should be needed"
);
assert!(
!tool.needs_confirmation(&input_absolute, &project, cx),
"When always_allow_tool_actions is true, no confirmation should be needed for absolute paths"
);
});
}
#[gpui::test]
async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
// Set up a custom config directory for testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
// Test ui_text shows context for various paths
let test_cases = vec![
(
json!({
"display_description": "Update config",
"path": ".zed/settings.json",
"mode": "edit"
}),
"Update config (local settings)",
".zed path should show local settings context",
),
(
json!({
"display_description": "Fix bug",
"path": "src/.zed/local.json",
"mode": "edit"
}),
"Fix bug (local settings)",
"Nested .zed path should show local settings context",
),
(
json!({
"display_description": "Update readme",
"path": "README.md",
"mode": "edit"
}),
"Update readme",
"Normal path should not show additional context",
),
(
json!({
"display_description": "Edit config",
"path": "config.zed",
"mode": "edit"
}),
"Edit config",
".zed as extension should not show context",
),
];
for (input, expected_text, description) in test_cases {
cx.update(|_cx| {
let ui_text = tool.ui_text(&input);
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
// Create a project in /project directory
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test file outside project requires confirmation
let input_outside = json!({
"display_description": "Edit file",
"path": "/outside/file.txt",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_outside, &project, cx),
"File outside project should require confirmation"
);
});
// Test file inside project doesn't require confirmation
let input_inside = json!({
"display_description": "Edit file",
"path": "project/file.txt",
"mode": "edit"
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input_inside, &project, cx),
"File inside project should not require confirmation"
);
});
}
#[gpui::test]
async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
// Set up a custom data directory for testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/home/user/myproject", json!({})).await;
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
// Get the actual local settings folder name
let local_settings_folder = paths::local_settings_folder_relative_path();
// Test various config path patterns
let test_cases = vec![
(
format!("{}/settings.json", local_settings_folder.display()),
true,
"Top-level local settings file".to_string(),
),
(
format!(
"myproject/{}/settings.json",
local_settings_folder.display()
),
true,
"Local settings in project path".to_string(),
),
(
format!("src/{}/config.toml", local_settings_folder.display()),
true,
"Local settings in subdirectory".to_string(),
),
(
".zed.backup/file.txt".to_string(),
true,
".zed.backup is outside project".to_string(),
),
(
"my.zed/file.txt".to_string(),
true,
"my.zed is outside project".to_string(),
),
(
"myproject/src/file.zed".to_string(),
false,
".zed as file extension".to_string(),
),
(
"myproject/normal/path/file.rs".to_string(),
false,
"Normal file without config paths".to_string(),
),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {} - path: {}",
description,
path
);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) {
// Set up a custom data directory for testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
// Create test files in the global config directory
let global_config_dir = paths::config_dir();
fs::create_dir_all(&global_config_dir).unwrap();
let global_settings_path = global_config_dir.join("settings.json");
fs::write(&global_settings_path, "{}").unwrap();
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test global config paths
let test_cases = vec![
(
global_settings_path.to_str().unwrap().to_string(),
true,
"Global settings file should require confirmation",
),
(
global_config_dir
.join("keymap.json")
.to_str()
.unwrap()
.to_string(),
true,
"Global keymap file should require confirmation",
),
(
"project/normal_file.rs".to_string(),
false,
"Normal project file should not require confirmation",
),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {}",
description
);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
// Create multiple worktree directories
fs.insert_tree(
"/workspace/frontend",
json!({
"src": {
"main.js": "console.log('frontend');"
}
}),
)
.await;
fs.insert_tree(
"/workspace/backend",
json!({
"src": {
"main.rs": "fn main() {}"
}
}),
)
.await;
fs.insert_tree(
"/workspace/shared",
json!({
".zed": {
"settings.json": "{}"
}
}),
)
.await;
// Create project with multiple worktrees
let project = Project::test(
fs.clone(),
[
path!("/workspace/frontend").as_ref(),
path!("/workspace/backend").as_ref(),
path!("/workspace/shared").as_ref(),
],
cx,
)
.await;
// Test files in different worktrees
let test_cases = vec![
("frontend/src/main.js", false, "File in first worktree"),
("backend/src/main.rs", false, "File in second worktree"),
(
"shared/.zed/settings.json",
true,
".zed file in third worktree",
),
("/etc/hosts", true, "Absolute path outside all worktrees"),
(
"../outside/file.txt",
true,
"Relative path outside worktrees",
),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {} - path: {}",
description,
path
);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
".zed": {
"settings.json": "{}"
},
"src": {
".zed": {
"local.json": "{}"
}
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test edge cases
let test_cases = vec![
// Empty path - find_project_path returns Some for empty paths
("", false, "Empty path is treated as project root"),
// Root directory
("/", true, "Root directory should be outside project"),
// Parent directory references - find_project_path resolves these
(
"project/../other",
false,
"Path with .. is resolved by find_project_path",
),
(
"project/./src/file.rs",
false,
"Path with . should work normally",
),
// Windows-style paths (if on Windows)
#[cfg(target_os = "windows")]
("C:\\Windows\\System32\\hosts", true, "Windows system path"),
#[cfg(target_os = "windows")]
("project\\src\\main.rs", false, "Windows-style project path"),
];
for (path, should_confirm, description) in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert_eq!(
tool.needs_confirmation(&input, &project, cx),
should_confirm,
"Failed for case: {} - path: {}",
description,
path
);
});
}
}
#[gpui::test]
async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
// Test UI text for various scenarios
let test_cases = vec![
(
json!({
"display_description": "Update config",
"path": ".zed/settings.json",
"mode": "edit"
}),
"Update config (local settings)",
".zed path should show local settings context",
),
(
json!({
"display_description": "Fix bug",
"path": "src/.zed/local.json",
"mode": "edit"
}),
"Fix bug (local settings)",
"Nested .zed path should show local settings context",
),
(
json!({
"display_description": "Update readme",
"path": "README.md",
"mode": "edit"
}),
"Update readme",
"Normal path should not show additional context",
),
(
json!({
"display_description": "Edit config",
"path": "config.zed",
"mode": "edit"
}),
"Edit config",
".zed as extension should not show context",
),
];
for (input, expected_text, description) in test_cases {
cx.update(|_cx| {
let ui_text = tool.ui_text(&input);
assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
});
}
}
#[gpui::test]
async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
init_test(cx);
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/project",
json!({
"existing.txt": "content",
".zed": {
"settings.json": "{}"
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Test different EditFileMode values
let modes = vec![
EditFileMode::Edit,
EditFileMode::Create,
EditFileMode::Overwrite,
];
for mode in modes {
// Test .zed path with different modes
let input_zed = json!({
"display_description": "Edit settings",
"path": "project/.zed/settings.json",
"mode": mode
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_zed, &project, cx),
".zed path should require confirmation regardless of mode: {:?}",
mode
);
});
// Test outside path with different modes
let input_outside = json!({
"display_description": "Edit file",
"path": "/outside/file.txt",
"mode": mode
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input_outside, &project, cx),
"Outside path should require confirmation regardless of mode: {:?}",
mode
);
});
// Test normal path with different modes
let input_normal = json!({
"display_description": "Edit file",
"path": "project/normal.txt",
"mode": mode
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input_normal, &project, cx),
"Normal path should not require confirmation regardless of mode: {:?}",
mode
);
});
}
}
#[gpui::test]
async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
// Set up with custom directories for deterministic testing
let temp_dir = tempfile::tempdir().unwrap();
init_test_with_config(cx, temp_dir.path());
let tool = Arc::new(EditFileTool);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
// Enable always_allow_tool_actions
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.always_allow_tool_actions = true;
agent_settings::AgentSettings::override_global(settings, cx);
});
// Test that all paths that normally require confirmation are bypassed
let global_settings_path = paths::config_dir().join("settings.json");
fs::create_dir_all(paths::config_dir()).unwrap();
fs::write(&global_settings_path, "{}").unwrap();
let test_cases = vec![
".zed/settings.json",
"project/.zed/config.toml",
global_settings_path.to_str().unwrap(),
"/etc/hosts",
"/absolute/path/file.txt",
"../outside/project.txt",
];
for path in test_cases {
let input = json!({
"display_description": "Edit file",
"path": path,
"mode": "edit"
});
cx.update(|cx| {
assert!(
!tool.needs_confirmation(&input, &project, cx),
"Path {} should not require confirmation when always_allow_tool_actions is true",
path
);
});
}
// Disable always_allow_tool_actions and verify confirmation is required again
cx.update(|cx| {
let mut settings = agent_settings::AgentSettings::get_global(cx).clone();
settings.always_allow_tool_actions = false;
agent_settings::AgentSettings::override_global(settings, cx);
});
// Verify .zed path requires confirmation again
let input = json!({
"display_description": "Edit file",
"path": ".zed/settings.json",
"mode": "edit"
});
cx.update(|cx| {
assert!(
tool.needs_confirmation(&input, &project, cx),
".zed path should require confirmation when always_allow_tool_actions is false"
);
});
}
}

View File

@@ -116,7 +116,7 @@ impl Tool for FetchTool {
"fetch".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -55,7 +55,7 @@ impl Tool for FindPathTool {
"find_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -57,7 +57,7 @@ impl Tool for GrepTool {
"grep".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -45,7 +45,7 @@ impl Tool for ListDirectoryTool {
"list_directory".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -42,7 +42,7 @@ impl Tool for MovePathTool {
"move_path".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -33,7 +33,7 @@ impl Tool for NowTool {
"now".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -23,7 +23,7 @@ impl Tool for OpenTool {
"open".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true
}
fn may_perform_edits(&self) -> bool {

View File

@@ -19,7 +19,7 @@ impl Tool for ProjectNotificationsTool {
"project_notifications".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}
fn may_perform_edits(&self) -> bool {

View File

@@ -54,7 +54,7 @@ impl Tool for ReadFileTool {
"read_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -77,7 +77,7 @@ impl Tool for TerminalTool {
Self::NAME.to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
true
}

View File

@@ -24,7 +24,7 @@ impl Tool for ThinkingTool {
"thinking".to_string()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -6,6 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use cloud_llm_client::{WebSearchResponse, WebSearchResult};
use futures::{Future, FutureExt, TryFutureExt};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
@@ -17,7 +18,6 @@ use serde::{Deserialize, Serialize};
use ui::{IconName, Tooltip, prelude::*};
use web_search::WebSearchRegistry;
use workspace::Workspace;
use zed_llm_client::{WebSearchResponse, WebSearchResult};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WebSearchToolInput {
@@ -32,7 +32,7 @@ impl Tool for WebSearchTool {
"web_search".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity<Project>, _: &App) -> bool {
false
}

View File

@@ -18,6 +18,6 @@ collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] }
util.workspace = true
workspace-hack.workspace = true

View File

@@ -3,12 +3,9 @@ use std::{io::Cursor, sync::Arc};
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, AssetSource, Global};
use rodio::{
Decoder, Source,
source::{Buffered, SamplesConverter},
};
use rodio::{Decoder, Source, source::Buffered};
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
pub struct SoundRegistry {
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
@@ -48,7 +45,7 @@ impl SoundRegistry {
.with_context(|| format!("No asset available for path {path}"))??
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
let source = Decoder::new(cursor)?.buffered();
self.cache.lock().insert(name.to_string(), source.clone());

View File

@@ -1,7 +1,7 @@
use assets::SoundRegistry;
use derive_more::{Deref, DerefMut};
use gpui::{App, AssetSource, BorrowAppContext, Global};
use rodio::{OutputStream, OutputStreamHandle};
use rodio::{OutputStream, OutputStreamBuilder};
use util::ResultExt;
mod assets;
@@ -37,8 +37,7 @@ impl Sound {
#[derive(Default)]
pub struct Audio {
_output_stream: Option<OutputStream>,
output_handle: Option<OutputStreamHandle>,
output_handle: Option<OutputStream>,
}
#[derive(Deref, DerefMut)]
@@ -51,11 +50,9 @@ impl Audio {
Self::default()
}
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
if self.output_handle.is_none() {
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
self.output_handle = output_handle;
self._output_stream = _output_stream;
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
}
self.output_handle.as_ref()
@@ -69,7 +66,7 @@ impl Audio {
cx.update_global::<GlobalAudio, _>(|this, cx| {
let output_handle = this.ensure_output_exists()?;
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
output_handle.play_raw(source).log_err()?;
output_handle.mixer().add(source);
Some(())
});
}
@@ -80,7 +77,6 @@ impl Audio {
}
cx.update_global::<GlobalAudio, _>(|this, _| {
this._output_stream.take();
this.output_handle.take();
});
}

View File

@@ -126,7 +126,7 @@ impl ChannelMembership {
proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::Invitee => 1,
},
username_order: self.user.github_login.as_str(),
username_order: &self.user.github_login,
}
}
}

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