Compare commits

..

393 Commits

Author SHA1 Message Date
Cole Miller
f4d838bb14 refactor agent server settings to store commands uniformly
Co-authored-by: Nia Espera <nia-e@haecceity.cc>
2025-09-04 18:12:27 -04:00
Marshall Bowers
4c32d5bf13 snippets: Disable feature_paths by default (#37565)
This PR updates the default configuration of the `snippets` extension to
disable suggesting paths (`feature_paths`).

If users want to enable it, it can be done via the settings:

```json
{
  "lsp": {
    "snippet-completion-server": {
      "settings": {
        "feature_paths": true
      }
    }
  }
}
```

Release Notes:

- N/A
2025-09-04 19:35:48 +00:00
Kirill Bulatov
ccae033d85 Make fallback open picker more intuitive (#37564)
Closes https://github.com/zed-industries/zed/issues/34991

Before, the picker did not allow to open the current directory that was
just completed:

<img width="553" height="354" alt="image"
src="https://github.com/user-attachments/assets/e77793c8-763e-416f-9728-18d5a39b467f"
/>

pressing `enter` here would open `assets`; pressing `tab` would append
the `assets/` segment to the query.
Only backspace, removing `/` would allow to open the current directory.

After:
<img width="574" height="349" alt="image"
src="https://github.com/user-attachments/assets/bdbb3e23-7c7a-4e12-8092-51a6a0ea9f87"
/>

The first item is now a placeholder for opening the current directory
with `enter`.
Any time a fuzzy query is appended, the placeholder goes away; `tab`
selects the entry below the placeholder.

Release Notes:

- Made fallback open picker more intuitive

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-09-04 19:34:23 +00:00
Marshall Bowers
c2fa9d7981 docs: Add configuration example for simple-completion-language-server (#37566)
This PR adds a configuration example for the
`simple-completion-language-server`.

We show the user how to re-enable the `feature_paths` option, as we're
now disabling it by default
(https://github.com/zed-industries/zed/pull/37565).

Release Notes:

- N/A
2025-09-04 19:25:52 +00:00
Anthony Eid
5f03202b5c settings ui: Create settings key trait (#37489)
This PR separates out the associated constant `KEY` from the `Settings`
trait into a new trait `SettingsKey`. This allows for the key trait to
be derived using attributes to specify the path so that the new
`SettingsUi` derive macro can use the same attributes to determine top
level settings paths thereby removing the need to duplicate the path in
both `Settings::KEY` and `#[settings_ui(path = "...")]`

Co-authored-by: Ben Kunkle <ben@zed.dev>

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-09-04 15:19:02 -04:00
Kirill Bulatov
223fda2fe2 Make remote projects to sync in local user settings (#37560)
Closes https://github.com/zed-industries/zed/issues/20024
Closes https://github.com/zed-industries/zed/issues/23489



https://github.com/user-attachments/assets/6466e0c1-4188-4980-8bb6-52ef6e7591c9


Release Notes:

- Made remote projects to sync in local user settings
2025-09-04 19:05:21 +00:00
Marshall Bowers
a85946eba8 docs: Update TOML docs (#37561)
This PR updates the TOML docs to remove references to Taplo and suggest
the Tombi extension for users wanting language server support.

Relates to https://github.com/zed-industries/zed/issues/36766.

Release Notes:

- N/A
2025-09-04 18:54:32 +00:00
Cole Miller
9d94358971 acp: Keep diff editors in sync with AgentFontSize global (#37559)
Release Notes:

- agent: Fixed `cmd-+` and `cmd--` not affecting the font size of diffs.
2025-09-04 18:33:56 +00:00
Marshall Bowers
9e11105483 toml: Extract to zed-extensions/toml repository (#37558)
This PR extracts the TOML extension to the
[zed-extensions/toml](https://github.com/zed-extensions/toml)
repository.

Release Notes:

- N/A
2025-09-04 18:07:50 +00:00
Anthony Eid
caebd0cc4d debugger: Fix stack frame filter crash (#37555)
The crash was caused by not accounting for the fact that a range of
collapse frames only counts as one entry. Causing the filter indices to
overshoot for indices after collapse frames (it was counting all
collapse frames instead of just one).

The test missed this because it all happened in one `cx.update` closure
and didn't render the stack frame list when the filter was applied. The
test has been updated to account for this.


Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-04 17:55:20 +00:00
Marshall Bowers
6e2922367c Use full SHA for blade dependency (#37554)
In https://github.com/zed-industries/zed/pull/37516 we updated the
`blade` dependency, but used a short SHA.

No reason to not use the full SHA.

Release Notes:

- N/A
2025-09-04 17:41:47 +00:00
Jiqing Yang
25ee9b1013 Fix Wayland crash on AMD GPUs by updating Blade (#37516)
Updates blade-graphics from e0ec4e7 to bfa594e to fix GPU crashes on
Wayland with AMD graphics cards.

The crash was caused by incorrect BLAS scratch buffer alignment - the
old version hardcoded 256-byte alignment, but AMD GPUs require different
alignment values. The newer Blade version uses the GPU's actual
alignment requirements instead of hardcoding.

Closes #37448

Release Notes:

- Migrate to newer version of Blade upstream
2025-09-04 17:21:44 +00:00
Ben Brandt
0870a1fe80 acp: Don't share API key with Anthropic provider (#37543)
Since Claude Code has it's own preferred method of grabbing API keys, we
don't want to reuse this one.

Release Notes:

- acp: Don't share Anthropic API key from the Anthropic provider to
allow default Claude Code login options

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-04 16:01:50 +00:00
Lukas Wirth
e37efc1e9b diagnostics: Fix diagnostics pane clearing up too eagerly on typing (#37546)
Closes https://github.com/zed-industries/zed/issues/30494

Release Notes:

- Fixed diagnostics pane closing buffers too eagerly when typing inside
it
2025-09-04 15:30:23 +00:00
Nathan Sobo
1ae326432e Extract a scheduler crate from GPUI to enable unified integration testing of client and server code (#37326)
Extracts and cleans up GPUI's scheduler code into a new `scheduler`
crate, making it pluggable by external runtimes. This will enable
deterministic integration testing with cloud components by providing a
unified test scheduler across Zed and backend code. In Zed, it will
replace the existing GPUI scheduler for consistent async task management
across platforms.

## Changes

- **Core Implementation**: `TestScheduler` with seed-based
randomization, session tracking (`SessionId`), and foreground/background
task separation for reproducible testing.
- **Executors**: `ForegroundExecutor` (!Send, thread-local) and
`BackgroundExecutor` (Send, with blocking/timeout support) as
GPUI-compatible wrappers.
- **Clock and Timer**: Controllable `TestClock` and future-based `Timer`
for time-sensitive tests.
- **Testing APIs**: `once()`, `with_seed()`, and `many()` methods for
configurable test runs.
- **Dependencies**: Added `async-task`, `chrono`, `futures`, etc., with
updates to `Cargo.toml` and lock file.

## Benefits

- **Integration Testing**: Facilitates reliable async tests involving
cloud sessions, reducing flakiness via deterministic execution.
- **Pluggability**: Trait-based design (`Scheduler`) allows easy
integration into non-GPUI runtimes while maintaining GPUI compatibility.
- **Cleanup**: Refactors GPUI scheduler logic for clarity, correctness
(no `unwrap()`, proper error handling), and extensibility.

Follows Rust guidelines; run `./script/clippy` for verification.

- [x] Define and test a core scheduler that we think can power our cloud
code and GPUI
- [ ] Replace GPUI's scheduler


Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-09-04 17:14:53 +02:00
张小白
a05f86f97b windows: Don't log error when RedrawWindow (#37542)
Release Notes:

- N/A
2025-09-04 14:47:17 +00:00
Marshall Bowers
473bbd78cc onboarding: Fix typos in comments (#37541)
This PR fixes some grammatical typos in some comments in the
`onboarding` crate.

Release Notes:

- N/A
2025-09-04 13:46:40 +00:00
张小白
28c78d2d85 windows: Keep just one copy of GPU instance (#37445)
Now we only keep a single copy of the GPU device. The GPU lost handling
got broken after #35376, but it’s properly handled again now.

Release Notes:

- N/A
2025-09-04 21:31:12 +08:00
Lukas Wirth
fca44f89c1 languages: Allow installing pre-release of rust-analyzer and clangd (#37530)
Release Notes:

- Added lsp binary config to allow fetching nightly rust-analyzer and
clangd releases
2025-09-04 09:22:19 +00:00
Mitch (a.k.a Voz)
b7ad20773c worktree: Create parent directories on rename (#37437)
Closes https://github.com/zed-industries/zed/issues/37357

Release Notes:

- Allow creating sub-directories when renaming a file in file finder

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-09-04 08:25:47 +00:00
Finn Evers
aa1629b544 Remove some unused events (#37498)
This PR cleans up some emitted events around the codebase. These events
are either never emitted or never listened for.

It seems better to re-implement these at some point should they again be
needed - this ensures that they will actually be fired in the cases
where they are needed as opposed to being there and getting unreliable
and stale (which is already the case for the majority of the events
removed here).

Lastly, this ensures the `CapabilitiesChanged` event is not fired too
often.

Release Notes:

- N/A
2025-09-04 09:09:28 +02:00
James Tucker
69a5c45672 gpui: Fix out-of-bounds node indices in dispatch_path (#37252)
Observed in a somewhat regular startup crash on Windows at head (~50% of
launches in release mode).

Closes #37212

Release Notes:

- N/A
2025-09-03 23:18:23 -07:00
沈瑗杰
d0aaf04673 Change DeepSeek max token count to 128k (#36864)
https://api-docs.deepseek.com/zh-cn/news/news250821

Now the official API supports 128k token content

and have modify the name to v3.1/v3.1 thinking

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-04 05:51:48 +00:00
Francis
d677c98f43 agent2: Use inline enums in now and edit_file tools JSON schema (#37397)
Added schemars annotations to generate inline enums instead of
references ($ref) in the JSON schema passed to LLMs.

Concerns :
- "timezeone" parameter for "now" tool function
- "mode" parameter for "edit_file" tool function

Should be the same for futures tools/functions enums. This is easier for
LLMs to understand the schema since many of them don't use JSON
references correctly.

Tested with :
- local GPT-OSS-120b with llama.cpp server (openai compatible)
- remote Claude Sonnet 4.0 with Zed pro subscription

Thanks in advance for the merge.
(notice this is my first PR ever on Github, I hope I'm doing things
well, please let me know if you have any comment - edit: just noticed my
username/email were not correctly setup on my local git, sorry, it's
been 5 years I've not used git)

Closes #37389

Release Notes:

- agent: Improve "now" and "edit_file" tool schemas to work with more
models.
2025-09-04 05:39:55 +00:00
Ben Brandt
ce362864db docs: Update OpenAI-compatible provider config format (#37517)
The example was still showing how we used to setup openai compatible
providers, but that format should only be used for changing the url for
your actual OpenAI provider.

If you are doing a compatible provider, it should be using the new
format.

Closes #37093

Release Notes:

- N/A
2025-09-04 04:39:06 +00:00
Umesh Yadav
3c021d0890 language_models: Fix beta_headers for Anthropic custom models (#37306)
Closes #37289

The current implementation has a problem. The **`from_id` method** in
the Anthropic crate works well for predefined models, but not for custom
models that are defined in the settings. This is because it fallbacks to
using default beta headers, which are incorrect for custom models.

The issue is that the model instance for custom models lives within the
`language_models` provider, so I've updated the **`stream_completion`**
method to explicitly accept beta headers from its caller. Now, the beta
headers are passed from the `language_models` provider all the way to
`anthropic.stream_completion`, which resolves the issue.

Release Notes:

- Fixed a bug where extra_beta_headers defined in settings for Anthropic
custom models were being ignored.

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-04 06:02:13 +02:00
Ben Kunkle
f36a545a86 onboarding: Improve performance of AI upsell card (#37504)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-04 00:03:32 +00:00
Anthony Eid
9eeeda1330 onboarding: Add telemetry to Basics page (#37502)
- Welcome Keymap Changed
- Welcome Theme Changed
- Welcome Theme mode Changed
- Welcome Page Telemetry Diagnostics Toggled
- Welcome Page Telemetry Metrics Toggled
- Welcome Vim Mode Toggled
- Welcome Keymap Changed
- Welcome Sign In Clicked

cc: @katie-z-geer 

Release Notes:

- N/A
2025-09-03 20:02:57 -04:00
Agus Zubiaga
da2d791127 Update external agents installation docs (#37500) 2025-09-03 21:02:34 -03:00
Agus Zubiaga
d6f0811dab acp: Receive available commands over notifications (#37499)
See: https://github.com/zed-industries/agent-client-protocol/pull/62

Release Notes:

- Agent Panel: Fixes an issue where Claude Code would timeout waiting
for slash commands to be loaded

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-03 22:24:59 +00:00
Ben Kunkle
be0bb4a56b Centralize ZED_STATELESS (#37492)
Closes #ISSUE

Centralizes the references to the `ZED_STATELESS` env var into a single
location in a new crate named `zed_env_vars`

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-03 22:10:14 +00:00
Danilo Leal
bf1ae1d196 docs: Fix typo in the CLAUDE.md section (#37497)
Follow-up to https://github.com/zed-industries/zed/pull/37496. Fix a
typo and improves writing overall.

Release Notes:

- N/A
2025-09-03 18:46:35 -03:00
Danilo Leal
3b7dbb87b0 docs: Add note about CLAUDE.md usage (#37496)
Some users asked whether Claude Code in Zed can also observe/consume
`CLAUDE.md` guidelines, regardless of whether they're at the root
`.claude` directory or within the project. Answer is yes and the
documentation will mention it now!

Release Notes:

- N/A
2025-09-03 18:31:54 -03:00
Max Brunsfeld
bb13228ad5 Revert "Remote: Change "sh -c" to "sh -lc" (#36760)" (#37417)
This reverts commit bf5ed6d1c9.

We believe this may be breaking some users whose shell initialization
scripts change the working directory.

Release Notes:

- N/A
2025-09-03 14:24:32 -07:00
Danilo Leal
ec1528b890 thread view: Refine the terminal tool card header UI (#37488)
Rendering the disclosure button last (on the far right of the header
container) to avoid awkward layouts when there's truncation and elapsed
time information being displayed.

Release Notes:

- N/A
2025-09-03 18:09:59 -03:00
Danilo Leal
2aa0114b40 ai onboarding: Add some fast-follow adjustments (#37486)
Closes https://github.com/zed-industries/zed/issues/37305

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-09-03 17:59:12 -03:00
localcc
bb2d833373 Revert "gpui: Fix overflow_hidden to support clip with border radius" (#37480)
This reverts commit 40199266b6.

The issue with the commit is: ContentMask<Pixels>::intersect is doing
intersection of corner radii which makes inner containers use the max
corner radius out of all the parents when it should be more complex to
correctly clip children (clip sorting..?)

Release Notes:

- N/A
2025-09-03 19:52:47 +00:00
Cole Miller
eedfc5be5a acp: Improve handling of invalid external agent server downloads (#37465)
Related to #37213, #37150

When listing previously-downloaded versions of an external agent, don't
try to use any downloads that are missing the agent entrypoint
(indicating that they're corrupt/unusable), and delete those versions,
so that we can attempt to download the latest version again.

Also report clearer errors when failing to start a session due to an
agent server entrypoint or root directory not existing.

Release Notes:

- N/A
2025-09-03 15:47:39 -04:00
Agus Zubiaga
0e76cc8036 acp: Display a new version call out when one is available (#37479)
<img width="500" alt="CleanShot 2025-09-03 at 16 13 59@2x"
src="https://github.com/user-attachments/assets/beb91365-28e2-4f87-a2c5-7136d37382c7"></img>



Release Notes:

- Agent Panel: Display a callout when a new version of an external agent
is available

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-09-03 19:39:04 +00:00
Ben Kunkle
6bd5251882 settings_ui: Add test for default values (#37466)
Closes #ISSUE

Adds a test that checks that all settings have default values in
`default.json`. Currently only tests that settings supported by
SettingsUi have defaults, as more settings are added to the settings
editor they will be added to the test as well.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-03 15:25:30 -04:00
Smit Barmase
13de400a2a editor: Do not correct text contrast on non-opaque editor (#37471)
We don’t know the background color behind a non-opaque editor, so we
should skip contrast correction in that case. This prevents
single-editor mode (which is always transparent) from showing weird text
colors when text is selected.

We can’t account for the actual background during contrast correction
because we compute contrast outside gpui, while the actual color
blending happens inside gpui during drawing.

<img width="522" height="145" alt="image"
src="https://github.com/user-attachments/assets/6ee71475-f666-482d-87e6-15cf4c4fceef"
/>

Release Notes:

- Fixed an issue where Command Palette text looked faded when selected.
2025-09-04 00:03:48 +05:30
Danilo Leal
c3480c3d6f docs: Update external agents content (#37413)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-03 09:59:49 -05:00
Kirill Bulatov
0cbacb8500 Make word deletions less greedy (#37352)
Closes https://github.com/zed-industries/zed/issues/37144

Adjusts `editor::DeleteToPreviousWordStart`,
`editor::DeleteToNextWordEnd`, `editor::DeleteToNextSubwordEnd` and
`editor::DeleteToPreviousSubwordStart` actions to

* take whitespace sequences with length >= 2 into account and stop after
removing them (whilst movement would also include the word after such
sequences)

* take current language's brackets into account and stop after removing
the text before them

The latter is configurable and can be disabled with `"ignore_brackets":
true` parameter in the action.

Release Notes:

- Improved word deletions to consider whitespace sequences and brackets
by default
2025-09-03 17:48:17 +03:00
Moritz von Göwels
7327ef662b terminal_view: Fix focusing of center-pane terminals (#37359)
With `reveal_stragegy=always` + `reveal_target=center`,
`TerminalPanel::spawn_task` activates & focuses the pane of the task.
This works fine in the terminal pane but doesn't for
`reveal_target=center`.

Please note: I'm not verified familiar with the architecture and
internal APIs of zed. If there's a better way or if this fix is a bad
idea, I'm fine with adapting this 😃

Closes #35908

Release Notes:

- Fixed task focus when re-spawning a task with `reveal_target=center`

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-09-03 14:23:46 +00:00
Lukas Wirth
c1ca7303a8 editor: Make blame and inline blame work for multibuffers (#37366)
Release Notes:

- Added blame view and inline blame support for multi buffer editors

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-09-03 14:22:35 +00:00
localcc
92283285ae Fix rendering on devices that don't support MapOnDefaultTextures (#37456)
Closes #37231

Release Notes:

- N/A
2025-09-03 14:14:56 +00:00
Lukas Wirth
d80f9dda75 languages: Fix python tasks failing when binary contains whitespaces (#37454)
Fixes https://github.com/zed-industries/zed/issues/33459

Release Notes:

- Fixed python tasks failing when the python binary path contains
whitespaces
2025-09-03 16:11:36 +02:00
Nia
ebc22c290b gpui: Don't risk accidentally panicking during tests (#37457)
See the failure in
https://github.com/zed-industries/zed/actions/runs/17413839503/job/49437345296

Release Notes:

- N/A
2025-09-03 15:44:07 +02:00
Bennet Bo Fenner
7633bbf55a acp: Fix issue with claude code /logout command (#37452)
### First issue

In the scenario where you have an API key configured in Zed and you run
`/logout`, clicking on `Use Anthropic API Key` would show `Method not
implemented`.

This happened because we were only intercepting the `Use Anthropic API
Key` click if the provider was NOT authenticated, which would not be the
case when the user has an API key set.

### Second issue

When clicking on `Reset API Key` the modal would be dismissed even
though you picked no Authentication Method (which means you still would
be unauthenticated)

---

This PR fixes both of these issues

Release Notes:

- N/A
2025-09-03 12:08:48 +00:00
Bennet Bo Fenner
91cbb2ec25 Add onboarding banner for claude code support (#37443)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-03 10:59:14 +00:00
Jason Lee
40199266b6 gpui: Fix overflow_hidden to support clip with border radius (#35083)
Release Notes:

- N/A

---

Same case in HTML example:


https://developer.mozilla.org/en-US/play?id=p7FhB3JAhiVfLHAXnsbrn7JYYX%2Byq1gje%2B%2BTZarnXvvjmaAx3NlrXqMAoI35s4zeakShKee6lydHYeHr

```html
<div style="padding: 50px; text-align: center;">
  <div style="overflow: hidden; border-radius: 24px">
    <div style="background: #000; border: 3px solid red; color: #fff; padding: 8px 28px;">
      Let build applications with GPUI.
    </div>
    <div style="background: #333; border: 3px dashed black; color: #fff; padding: 8px 28px;">
      Let build applications with GPUI.
    </div>
  </div>

  <div style="margin-top: 20px; border-radius: 24px">
    <div style="background: #000; color: #fff; padding: 8px 28px;">
      This is not overflow: hidden.
    </div>
  </div>
</div>
```

<img width="610" height="213" alt="image"
src="https://github.com/user-attachments/assets/5f95e263-e52c-414f-8f0c-e6aa04ceb802"
/>

### Before

<img width="912" height="740" alt="image"
src="https://github.com/user-attachments/assets/f09c1936-52fc-4381-9a50-93977e9d64a6"
/>

### After 

```bash
cargo run -p gpui --example content_mask
```

<img width="912" height="740" alt="image"
src="https://github.com/user-attachments/assets/4bde58f3-c850-418d-9dc7-d2245852e7d7"
/> |


- [x] Metal
- [x] Blade
- [x] DirectX
- [x] ContentMask radius must reduce the container border widths.
- [x] The dash border render not correct, when not all side have
borders.
2025-09-03 12:44:33 +02:00
Danilo Leal
9a8c5053c2 agent: Update message editor placeholder (#37441)
Release Notes:

- N/A
2025-09-03 06:54:31 -03:00
localcc
c446662862 Fix font rendering at very large scales (#37440)
Release Notes:

- Fixed fonts disappearing at very large scales on windows
2025-09-03 09:21:45 +00:00
Finn Evers
6feae92616 rust: Improve highlighting in derive macros (#37439)
Follow-up to https://github.com/zed-industries/zed/pull/37049

This fixes an issue where we would lose highlighting in derive macros if
one of the names was qualified.

| Before | After |
| --- | --- |
| <img width="886" height="398" alt="Bildschirmfoto 2025-09-03 um 10 39
25"
src="https://github.com/user-attachments/assets/dbc680e3-6ce3-4059-9934-9daa4c59d4a0"
/> | <img width="886" height="398" alt="Bildschirmfoto 2025-09-03 um 10
38 14"
src="https://github.com/user-attachments/assets/6e10df6f-5158-4bfd-81ab-8f2b384f1e99"
/> |


Release Notes:

- N/A
2025-09-03 09:02:21 +00:00
Cole Miller
ae840c6ef3 acp: Fix handling of single-file worktrees (#37412)
When the first visible worktree is a single-file worktree, we would
previously try to use the absolute path of that file as the root
directory for external agents, causing an error. This PR changes how we
handle this situation: we'll use the root of the first non-single-file
visible worktree if there are any, and if there are none, the parent
directory of the first single-file visible worktree.

Related to #37213

Release Notes:

- acp: Fixed being unable to run external agents when a single file (not
part of a project) was opened in Zed.
2025-09-03 03:40:14 -04:00
Michael Sloan
d7fd5910d7 Use slice from Rope chunk when possible while iterating lines (#37430)
Release Notes:

- N/A
2025-09-03 06:35:31 +00:00
Kirill Bulatov
8d5861322b Allow wrapping markdown text into * by selecting text and writing the * (#37426)
Release Notes:

- Allowed wrapping markdown text into `*` by selecting text and writing
the `*`
2025-09-03 05:50:53 +00:00
Jakub Konka
5a9e18603d gpui: Fix intra rustdoc links (#37320)
The only warnings remaining are links to private modules/items, but I
lack knowledge to work out if the referenced modules/items should be
made public, or if the links should be rewritten into exposed
traits/items.

Links to associated items such as trait implementations have to be
written using full markdown format such as:

... [[ `App::update_global` ]](( BorrowAppContext::update_global ))

This is due to https://github.com/rust-lang/rust/issues/74563 which
sadly prohibits fully-qualified syntax:

... [[ `<App as BorrowAppContext>::update_global` ]]

Release Notes:

- N/A

Probably related to https://github.com/zed-industries/zed/pull/37072
2025-09-03 07:31:48 +02:00
chris
2a7761fe17 Instruct macOS users to run xcodebuild -downloadComponent MetalToolchain (#37411)
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-02 21:36:36 -07:00
Max Brunsfeld
f23096034b Remove wsl command line args on non-windows platforms (#37422)
Release Notes:

- N/A
2025-09-03 03:49:04 +00:00
Peter Tripp
1ed17fdd94 Bump Zed to v0.204 (#37415)
Release Notes:

-N/A
2025-09-02 21:00:19 -04:00
Marshall Bowers
7ea7f4e767 reqwest_client: Remove example (#37410)
This PR removes the example from the `reqwest_client` crate, as it
doesn't seem worth maintaining.

Release Notes:

- N/A
2025-09-03 00:52:04 +00:00
Peter Tripp
035d7ddcf8 ci: Skip Nix for commits on release branches and tags (#37407)
When doing stable/preview releases simultaneously there are two tags and
two branches pushed. Previously nix was attempting 1 job for each. Our
current mac parallelism is 4.
 
Can't easily test this. 🤷 

Release Notes:

- N/A
2025-09-02 20:37:40 -04:00
Danilo Leal
9d67276090 agent: Fix cut off slash command descriptions (#37408)
Release Notes:

- N/A
2025-09-03 00:28:35 +00:00
Richard Feldman
161d128d45 Handle model refusal in ACP threads (#37383)
If the model refuses a prompt, we now:
* Show an error if it was a user prompt (and truncate it out of the
history)
* Respond with a failed tool call if the refusal was for a tool call

<img width="607" height="260" alt="Screenshot 2025-09-02 at 5 11 45 PM"
src="https://github.com/user-attachments/assets/070b5ee7-6ad6-4a63-8395-f9a5093cc40e"
/>
<img width="607" height="265" alt="Screenshot 2025-09-02 at 5 11 38 PM"
src="https://github.com/user-attachments/assets/98862586-390b-494e-b1f8-71d8341c8d9d"
/>



Release Notes:

- Improve handling of model refusals in ACP threads
2025-09-02 20:25:10 -04:00
Cole Miller
e1b0a98c34 ci: Remove Windows crash analysis CI scripts (#36694)
We'll just SSH into the Windows runners and look for crashes there.

Reverts #35926 

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-03 00:24:00 +00:00
Rafał Krzyważnia
ae0ee70abd Add configurable timeout for context server tool calls (#33348)
Closes: #32668

- Add
[tool_call_timeout_millis](https://github.com/cline/cline/pull/1904)
field to ContextServerCommand, like in Cline
- Update ModelContextServerBinary to include timeout configuration
- Modify Client to store and use configurable request timeout
- Replace hardcoded REQUEST_TIMEOUT with self.request_timeout
- Rename REQUEST_TIMEOUT to DEFAULT_REQUEST_TIMEOUT for clarity
- Maintain backward compatibility with 60-second default

Release Notes:

- context_server: Add support for configurable timeout for MCP tool
calls

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-03 00:03:56 +00:00
versecafe
893eb92f91 docs: Note edge case for macOS 26 (#37392)
- I believe this is caused by metal not being found due to it being on
the XcodeBeta path, not sure if there's a better fix for this but it'll
work until 26 is the latest release

Release Notes:

- N/A
2025-09-02 19:40:07 -04:00
Vitaly Slobodin
45fa6d81ac tailwind: Add HTML+ERB to the list of supported languages (#36797)
Hi! As part of https://github.com/zed-extensions/ruby/issues/162 we
would like to rename HTML/ERB to HTML+ERB since it is more syntactically
correct to treat such language as ERB on top of HTML rather than HTML or
ERB.

To keep the user experience intact, we outlined the prerequisites in the
linked issue. This is the first PR that adds the HTML+ERB language name
to the list of enabled languages for the Emmet extension. We will do the
same for the Tailwind configuration in the Zed codebase. Once the new
versions of Emmet and Zed are released, we will merge the pull request
in the Ruby extension repository and release the updated version. After
that, we will remove the old HTML/ERB and YAML/ERB languages. Let me
know if that sounds good. Thanks!

Release Notes:

- N/A

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-09-02 23:32:43 +00:00
Ben Brandt
60ad82cc94 Fix typo in clippy lint name (#37405)
Release Notes:

- N/A
2025-09-02 23:30:32 +00:00
Cole Miller
564ded71c1 acp: Disable external agents over SSH (#37402)
Follow-up to #37377 

Show a clearer error here until SSH support is implemented.

Release Notes:

- N/A
2025-09-02 19:29:21 -04:00
Umesh Yadav
63b3839a83 language_models: Prevent sending the tools object to unsupported models for Ollama (#37221)
Closes #32758

Release Notes:

- Resolved an issue with the Ollama provider that caused requests to
fail with a 400 error for models that don't support tools. The tools
object is now only sent to compatible models to ensure successful
requests.
2025-09-03 01:28:36 +02:00
Umesh Yadav
9f749881b3 language_models: Fix tool_choice null issue for other providers (#34554)
Follow up: #34532

Closes #35434 

Mostly fixes a issue were when the tool_choice is none it was getting
serialised as null. This was fixed for openrouter just wanted to follow
up and cleanup for other providers which might have this issue as this
is against the spec.

Release Notes:

- N/A
2025-09-03 01:22:57 +02:00
Danilo Leal
946efb03df Add option for code context menu items to have dynamic width (#37404)
Follow up to https://github.com/zed-industries/zed/pull/30598

This PR introduces the `display_options` field in the
`CompletionResponse`, allowing a code context menu width to be
dynamically dictated based on its larger item. This will allow us to
have the @-mentions and slash commands completion menus in the agent
panel not be bigger than it needs to be. It may also be relevant/useful
in the future for other use cases.

For now, we set all instances of code context menus to use a fixed
width, as defined in the PR linked above, which means this PR shouldn't
cause any visual change.

Release Notes:

- N/A

Co-authored-by: Michael Sloan <mgsloan+github@gmail.com>
2025-09-02 20:18:15 -03:00
Marshall Bowers
4b96ad3fba gpui: Remove http_client feature (#37401)
This PR removes the `http_client` feature from the `gpui` crate, as it
wasn't really doing anything.

It only controlled whether we depend on the `http_client` crate, but
from what I can tell we always depended on it anyways.

Obviates https://github.com/zed-industries/zed/pull/36615.

Release Notes:

- N/A
2025-09-02 23:14:47 +00:00
Umesh Yadav
4368c1b56b language_models: Add OpenRouterError and map OpenRouter errors to LanguageModelCompletionError (#34227)
Improves the error handling for openrouter and adds automatic retry like
anthropic for few of the status codes.
Release Notes:

- Improves error messages for Openrouter provider
- Automatic retry when rate limited or Server error from Openrouter
2025-09-03 01:13:46 +02:00
Dino
e5a968b709 vim: Fix change surround with any brackets text object (#37386)
This commit fixes an issue with how the `AnyBrackets` object was handled
with change surrounds (`cs`). With the keymap below, if one was to use
`csb{` with the text `(bracketed)` and the cursor inside the
parentheses, the text would not change.

```json
{
  "context": "vim_operator == a || vim_operator == i || vim_operator == cs",
  "bindings": {
    "b": "vim::AnyBrackets"
  }
}
```

Unfortunately there was no implementation for finding a corresponding
`BracketPair` for the `AnyBrackets` object, meaning that, when using
`cs` (change surrounds) the code would simply do nothing.

This commit updates this logic so as to try and find the nearest
surrounding bracket (parentheses, curly brackets, square brackets or
angle brackets), ensuring that `cs` also works with `AnyBrackets`.

Closes #24439

Release Notes:

- Fixed handling of `AnyBrackets` in vim's change surrounds (`cs`)
2025-09-02 16:03:14 -07:00
Ben Brandt
7aecab8e14 agent2: Only setup real client for real models (#37403)
Before we were setting up lots of test setup regardless of if we were
actually going to be making real requests or not.

This will hopefully help with intermittent test errors we're seeing on
Windows in CI.

Release Notes:

- N/A
2025-09-02 23:02:36 +00:00
Smit Barmase
e4df866664 editor: Do not show edit prediction during in-progress IME composition (#37400)
Closes #37249

We no longer show edit prediction when composing IME since it isn't
useful for unfinished alphabet.

Release Notes:

- Fixed edit predictions showing up during partial IME composition.
2025-09-03 03:41:10 +05:30
Bennet Bo Fenner
8770fcc841 acp: Enable claude code feature flag for everyone (#37390)
Release Notes:

- N/A
2025-09-02 17:57:29 -04:00
Smit Barmase
6dcae2711d terminal: Fix not able to select text during continuous output (#37395)
Closes #37211

Regressed in https://github.com/zed-industries/zed/pull/33305

Every time the terminal updates, we emit
`SearchEvent::MatchesInvalidated` to trigger a re-run of the buffer
search, which calls `clear_matches` to drop stale results.
https://github.com/zed-industries/zed/pull/33305 PR also cleared the
selection when clearing matches, which caused this issue. We could fix
it by only clearing matches and selection when they’re non-empty, but
it’s better to not clear the selection at all. This matches how the
editor behaves and keeps it consistent. This PR reverts that part of
code.


Release Notes:

- Fixed an issue where text selection was lost during continuous
terminal output.
2025-09-03 03:00:09 +05:30
Richard Feldman
5e01fb8f1c Nice errors for unsupported ACP slash commands (#37393)
If we get back slash-commands that aren't supported, tell the user that
this is the problem.

Release Notes:

- Improve error messages for unsupported ACP slash-commands

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-02 20:39:24 +00:00
Conrad Irwin
88a79750cc Disable external agents over collab (#37377)
Release Notes:

- Disable UI to boot external agents in collab projects (as they don't
work)
2025-09-02 12:53:53 -07:00
Umesh Yadav
4c411b9fc8 language_models: Make JsonSchemaSubset the default tool_input_format for the OpenAI-compatible provider (#34921)
Closes #30188
Closes #34911
Closes #34906

Many OpenAI-compatible providers do not automatically filter the tool
schema to comply with the underlying model's requirements; they simply
proxy the request. This creates issues, as models like **Gemini**,
**Grok**, and **Claude** (when accessed via LiteLLM on Bedrock) are
incompatible with Zed's default tool schema.

This PR addresses this by defaulting to a more compatible schema subset
instead of the full schema.

### Why this approach?

* **Avoids Poor User Experience:** One alternative was to add an option
for users to manually set the JSON schema for models that return a `400
Bad Request` due to an invalid tool schema. This was discarded as it
provides a poor user experience.
* **Simplifies Complex Logic:** Another option was to filter the schema
based on the model ID. However, as demonstrated in the attached issues,
this is unreliable. For instance, `claude-4-sonnet` fails when proxied
through LiteLLM on Bedrock. Reliably determining behavior would require
a non-trivial implementation to manage provider-and-model combinations.
* **Better Default Behavior:** The current approach ensures that tool
usage works out-of-the-box for the majority of cases by default,
providing the most robust and user-friendly solution.


Release Notes:

- Improved tool compatibility with OpenAI API-compatible providers

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-02 14:29:07 -04:00
Peter Tripp
5ac6ae501f docs: Link glossary (#37387)
Follow-up to: https://github.com/zed-industries/zed/pull/37360

Add glossary.md to SUMMARY.md so it's linked to the public
documentation.

Release Notes:

- N/A
2025-09-02 17:57:48 +00:00
Michael Sloan
c01f12b15d zeta: Small refactoring in license detection check - rfind instead of iterated ends_with (#37329)
Release Notes:

- N/A
2025-09-02 17:23:35 +00:00
Agus Zubiaga
dfa066dfe8 acp: Display slash command hints (#37376)
Displays the slash command's argument hint while it hasn't been
provided:


https://github.com/user-attachments/assets/f3bb148c-247d-43bc-810d-92055a313514


Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-02 16:39:55 +00:00
Richard Feldman
ac8c653ae6 Fix race condition between feature flag and deserialization (#37381)
Right now if you open Zed, and we deserialize an agent that's behind a
feature flag (e.g. CC), we don't restore it because the feature flag
check hasn't happened yet at the time we're deserializing (due to auth
not having finished yet).

This is a simple fix: assume that if you had serialized it in the first
place, you must have had the feature flag enabled, so go ahead and
reopen it for you.

Release Notes:

- N/A
2025-09-02 12:28:07 -04:00
Danilo Leal
d2318be8d9 terminal view: Hide inline assist button if AI is disabled (#37378)
Closes https://github.com/zed-industries/zed/issues/37372

Release Notes:

- Fix the terminal inline assistant button showing despite `disable_ai`
being turned on.

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-09-02 13:27:06 -03:00
Danilo Leal
a026163746 inline assistant: Adjust completion menu item font size (#37375)
Now the @ completion menu items font size respect/match the buffer's
font size, as opposed to being rendered a bit bigger.

| Before | After |
|--------|--------|
| <img width="1226" height="468" alt="Screenshot 2025-09-02 at 11 
09@2x"
src="https://github.com/user-attachments/assets/a6d37110-b544-40c3-bf7a-447ea003d4d7"
/> | <img width="1218" height="462" alt="Screenshot 2025-09-02 at 11  09
2@2x"
src="https://github.com/user-attachments/assets/19e58bf8-2db5-442e-8f60-02dd9ee1308f"
/> |

Release Notes:

- inline assistant: Improved @-mention menu item font size, better
matching the buffer's font size.
2025-09-02 13:26:56 -03:00
Marshall Bowers
ad3ddd381d Revert "gpui: Do not render ligatures between different styled text runs (#37175) (#37382)
This reverts commit 62083fe796.

We're reverting this as it causes layout shift when typing/selecting
with ligatures:


https://github.com/user-attachments/assets/80b78909-62f5-404f-8cca-3535c5594ceb

Release Notes:

- Reverted #37175
2025-09-02 16:18:49 +00:00
David Kleingeld
7e3fbeb59d Add the Glossary from the channel into Zed (#37360)
This should make it easier for contributors to learn all the terms used
in the Zed code base.

Release Notes:

- N/A
2025-09-02 15:59:58 +00:00
Jonathan Camp
8e7caa429d remove extra brace in rules template (#37356)
Release Notes:

- Fixed: remove extra brace in rules template
2025-09-02 15:26:12 +00:00
Dino
c894351544 vim: Fix change surrounding quotes with whitespace within (#37321)
This commit fixes a bug with Zed's vim mode surrounds plugin when
dealing with replacing pairs with quote and the contents between the
pairs had some whitespace within them.

For example, with the following string:

```
' str '
```

If one was to use the `cs'"` command, to replace single quotes with
double quotes, the result would actually be:

```
"str"
```

As the whitespace before and after the closing character was removed.

This happens because of the way the plugin decides whether to add or
remove whitespace after and before the opening and closing characters,
repsectively. For example, using `cs{[` yields a different result from
using `cs{]`, the former adds a space while the latter does not.

However, since for quotes the opening and closing character is exactly
the same, this behavior is not possible, so this commit updates the code
in `vim::surrounds::Vim.change_surrounds` so that it never adds or
removes whitespace when dealing with any type of quotes.

Closes #12247 

Release Notes:

- Fixed whitespace handling when changing surrounding pairs to quotes in
vim mode
2025-09-02 09:11:35 -06:00
Finn Evers
a96015b3c5 activity_indicator: Show extension installation and updates (#37374)
This PR fixes an issue where extension operations would never show in
the activity indicator despite this being implemented for ages. This
happened because we were always returning `None` whenever the app has a
global auto updater, which is always the case, so the code path for
showing extension updates in the indicator could never be hit despite
existing prior. Also slightly improves the messages shown for ongoing
extension operations, as these were previously context unaware.

While I was at this, I also quickly took a stab at cleaning up some
remotely related stuff, namely:
- The `AnimationExt` trait is now by default only implemented for
anything that also implements `IntoElement`. This prevents
`with_animation` from showing up for e.g. `u32` within the suggestions
(finally).
- Commonly used animations are now implemented in the
`CommonAnimationExt` trait within the `ui` crate so the needed code does
not always need to be copied and element IDs for the animations are
truly unique.

Relevant change here regarding the original issue is the change from the
`return match` to just a `match` within the activitiy indicator, which
solved the issue at hand.

If we find this to be too noisy at some point, we can easily revisit,
but I think this holds important enough information to be shown in the
activity indicator, especially whilst developing extensions.

Release Notes:

- Extension installation and updates will now be shown in the activity
indicator.
2025-09-02 16:51:13 +02:00
张小白
2eb7ac97e0 windows: Use a message-only window for WindowsPlatform (#37313)
Previously, we were using `PostThreadMessage` to pass messages to
`WindowsPlatform`. This PR switches to an approach similar to `winit`
which using a hidden window as the message window (I guess that’s why
winit uses a hidden window?). The difference is that this PR creates it
as a message-only window.

Thanks to @reflectronic for the original PR #37255, this implementation
just fits better with the current code style.


Release Notes:

- N/A

---------

Co-authored-by: reflectronic <john-tur@outlook.com>
2025-09-02 22:32:24 +08:00
张小白
f06c18765f Rename from create_ssh_worktree to create_remote_worktree (#37358)
This is a left-over issue of #37035

Release Notes:

- N/A
2025-09-02 14:12:24 +00:00
Max Brunsfeld
2f279c5de4 Fix small errors preventing WSL support from working (#37350)
On nightly, when I run `zed` under WSL, I get an error parsing the
shebang line

```
/usr/bin/env: ‘sh\r’: No such file or directory
```

I believe that this is because in CI, Git checks out the file with CRLF
line endings, and that is how it is copied into the installer.

Also, the file extension was incorrect when downloading the production
remote server (a gzipped binary), preventing extraction from working
properly.

Release Notes:

- N/A
2025-09-02 07:07:23 -07:00
localcc
60b95d9253 Use premultiplied alpha for emoji rendering (#37370)
This improves emoji rendering on windows removing artifacts at the edges
by using premultiplied alpha. A bit more context can be found in #37167

Release Notes:

- N/A
2025-09-02 13:59:27 +00:00
Cole Miller
47ad1b2143 agent2: Fix terminal tool call content not being shown once truncated (#37318)
We render terminals as inline if their content is below a certain line
count, and scrollable past that point. In the scrollable case we weren't
setting a height for the terminal's container, causing it to be rendered
at height 0, which means no lines would be displayed. This PR fixes that
by setting an explicit height for the scrollable case, like we do in the
agent1 UI code.

Release Notes:

- agent: Fixed a bug that caused terminals in the panel to be empty
after their content reached a certain size.
2025-09-02 09:03:11 -04:00
Lukas Wirth
35c0d02c7c project: Temporarily disable terminal activation scripts on windows (#37361)
They seem to break things on window right now

Release Notes:

- N/A
2025-09-02 10:42:29 +00:00
Bennet Bo Fenner
374a8bc4cb acp: Add support for slash commands (#37304)
Depends on
https://github.com/zed-industries/agent-client-protocol/pull/45

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-02 08:48:33 +00:00
Maksim Bondarenkov
f06be6f3ec docs: Add link to msys2 docs page (#37327)
it was removed earlier. better to keep this link because the page
contains some useful information

Release Notes:

- N/A
2025-09-02 07:02:41 +00:00
Ben Kunkle
970242480a settings_ui: Improve case handling (#37342)
Closes #ISSUE

Improves the derive macro for `SettingsUi` so that titles generated from
struct and field names are shown in title case, and toggle button groups
use title case for rendering, while using lower case/snake case in JSON

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-02 01:17:27 +00:00
Ben Kunkle
54cec5b484 settings_ui: Get editor settings working (#37330)
Closes #ISSUE

This PR includes the necessary work to get `EditorSettings` showing up
in the settings UI. Including making the `path` field on
`SettingsUiItem`'s optional so that top level items such as
`EditorSettings` which have `Settings::KEY = None` (i.e. are treated
like `serde(flatten)`) have their paths computed correctly for JSON
reading/updating.

It includes the first examples of a pattern I expect to continue with
the `SettingsUi` work with respect to settings reorganization, that
being adding missing defaults, and adding explicit values (or aliases)
to settings which previously relied on `null` being a value for optional
fields.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-02 00:26:42 +00:00
Ben Kunkle
60d17cccd3 settings_ui: Move settings UI trait to file content (#37337)
Closes #ISSUE

Initially, the `SettingsUi` trait was tied to `Settings`, however, given
that the `Settings::FileContent` type (which may be the same as the type
that implements `Settings`) will be the type that more directly maps to
the JSON structure (and therefore have the documentation, correct field
names (or `serde` rename attributes), etc) it makes more sense to have
the deriving of `SettingsUi` occur on the `FileContent` type rather than
the `Settings` type.

In order for this to work a relatively important change had to be made
to the derive macro, that being that it now "unwraps" options into their
inner type, so a field with type `Option<Foo>` where `Foo: SettingsUi`
will treat the field as if it were just `Foo`, expecting there to be a
default set in `default.json`. This imposes some restrictions on what
`Settings::FileContent` can be as seen in 1e19398 where `FileContent`
itself can't be optional without manually implementing `SettingsUi`, as
well as introducing some risk that if the `FileContent` type has
`serde(default)`, the default value will override the default value from
`default.json` in the UI even though it may differ (but it should!).

A future PR should probably replace the other settings with `FileContent
= Option<T>` (all of which currently have `T == bool`) with wrapper
structs and have `KEY = None` so the further niceties
`derive(SettingsUi)` will provide such as path renaming, custom UI, auto
naming and doc comment extraction can be used.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-01 18:42:33 -04:00
Ben Kunkle
8a8a9a4f07 settings_ui: Add dynamic settings UI item (#37331)
Closes #ISSUE

Adds a first draft of a way for "Dynamic" settings items to be added,
where Dynamic means settings where multiple sets of options are possible
(i.e. discriminated union, rust enum, etc). The implementation is very
similar to that of `Group`, except that instead of rendering all of it's
descendants, it contains a function to determine _which_ descendant to
render, whether that be a single item or a nested group of items.
Currently this is done in a type-unsafe way with indices, a future
improvement could be to make the API more type safe, and easier to
manually implement correctly.

An example of a "Dynamic" setting is `theme`, where it can either be a
string of the desired theme name, or an object with `mode: "light" |
"dark" | "system"` as well as theme names for `light` and `dark`. In the
system implemented by this PR, this would become a dynamic settings UI
item, where option `0` is a single item, the theme name selector, and
option `1` is a group, containing items for the `mode`, and
`light`/`dark` options.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-01 17:53:43 -04:00
Kirill Bulatov
634a1343dd Bump xcb dependency (#37335)
Deals with https://github.com/zed-industries/zed/security/dependabot/65

Release Notes:

- N/A
2025-09-01 21:51:15 +00:00
claytonrcarter
2ba25b5c94 editor: Support rewrap in block comments (#34418)
This updates `editor: rewrap` to work within doc comments, based on the
code that extends such comments on newline. I added some tests, and I've
tested it out in JS, C and PHP. (Though PHP depends on
https://github.com/zed-extensions/php/pull/40)

Closes #19794
Closes #18221

**Caveat:**
~~This will not rewrap an existing single-line block comment, such as
the one provided in #18221:~~ this will now rewrap as expected
```c
/* we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex */
```
However, it will rewrap a similar comment if it is shaped like a doc
comment. In other words, this will rewrap as expected:
```c
/* 
 * we can triangulate any convex polygon by picking a vertex and connecting it to the next two vertices; we first read two vertices, and then, for every subsequent vertex, we can form a triangle by connecting it to the first and previous vertex 
 */
```

This seems like a reasonable improvement and limitation to me,
especially as a first step.

cc @smitbarmase because I think that you've been making a lot of the
`newline` and `rewrap` changes recently. (Thank you for those, by the
way!)

Release Notes:

- Added support for rewrap in block comments.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-09-01 20:00:01 +00:00
Marshall Bowers
965dbc988f gpui: Fix typo in Windows alpha correction shader (#37328)
This PR fixes a typo in the Windows alpha correction shader that is now
caught by https://github.com/zed-industries/zed/pull/37314.

Another case that could be addressed by Bors.

Release Notes:

- N/A
2025-09-01 15:33:11 -04:00
Agus Zubiaga
5b73b40df8 ACP Terminal support (#37129)
Exposes terminal support via ACP and migrates our agent to use it.

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-01 18:57:15 +00:00
localcc
d910feac1d Implement perceptual gamma / contrast correction (#37167)
Closes #36023 

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

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

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

Release Notes:

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


Release Notes:

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

Release Notes:

- Add support for custom API Urls for DeepSeek Provider

---------

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

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

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

#### Before/after

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

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

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

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

Release Notes:

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

H/T: @brandon-fryslie

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

### GitHub Vulnerability Alerts

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

### Impact

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

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

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

### Patches

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

### Workarounds

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

### References

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

### Acknowledgments

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

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

---

### Release Notes

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

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

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

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

#### Impact

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

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

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

#### Solution

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

#### Affected Versions

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

#### Recommendations

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

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

#### Migration

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

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

#### Acknowledgments

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

---------

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

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

Code example:

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

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

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

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

Release Notes:

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

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

- Fixed auto size rendering of SVG images in Markdown.

## Before

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


## After

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

For GPUI example

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

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

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

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

## Todo

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

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

Release Notes:

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

---------

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


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

Into the agent2 code.

Release Notes:

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

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

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

Release Notes:

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

That broke tests.

Release Notes:

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

Release Notes:

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

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

### Structure

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

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

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

### Foundation 

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

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


#### Immediate Follow ups

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

Release Notes:

- N/A

---------

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

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

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

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

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

### Recording


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

---

Closes #13194 

Release Notes:

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

---------

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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

How it works:

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

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

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


Result:

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

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

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

Release Notes:

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

- N/A

---------

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

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

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

Release Notes:

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

Release Notes:

- N/A
2025-08-28 20:09:20 -06:00
Michael Sloan
cfd56a744d zeta: Show update required notification on appropriate window(s) (#37130)
To show these notifications, Zeta was being initialized with the initial
workspace it's used on - which may not even still exist! This removes a
confusing/misleading workspace field from Zeta.

Release Notes:

- N/A
2025-08-29 00:22:56 +00:00
Marshall Bowers
960d9ce48c Disable Expert language server by default for Elixir (#37126)
This PR updates the language server configuration for Elixir and HEEx to
not start the [Expert](https://github.com/elixir-lang/expert) language
server by default.

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

It makes tab switching feel even faster!

Release Notes:

- N/A

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

Release Notes:

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

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

Notes:

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

Release Notes:

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

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

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

Closes #ISSUE

Release Notes:

- N/A

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

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

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

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

Release Notes:

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

Changes:

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

Release Notes:

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

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

Release Notes:

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

## Synopsis

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

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


## Solution

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

Seems fully inline with the changes illustrated in #7276.


## Screenshots

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

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


## Changelog

Release Notes:

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

Release Notes:

- N/A

---------

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

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

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

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

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

Release Notes:

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

---------

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

Closes #37038 

Release Notes:

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

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

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

Release Notes:

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



Release Notes:

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

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

### Preview

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



Release Notes:

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

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



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



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



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



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


Release Notes:

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

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

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

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



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

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

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



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

---------

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

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

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

### Screen Capture


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

---

Closes #32390

Release Notes:

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

---------

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

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

Here's a preview of the feature in action:


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


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

Release Notes:

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

---------

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

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

- N/A
2025-08-27 15:59:08 -04:00
Joseph T. Lyons
d43cf2c486 Link out to release channel FAQ in Docs (#37029)
This PR links users to the FAQ on the release channels, which has more
in-depth coverage of the process.

Release Notes:

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

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

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

Release Notes:

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

---------

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

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

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


Release Notes:

- N/A
2025-08-27 18:12:34 +00:00
Floyd Wang
9ca4fb16b2 gpui: Support disabling window resizing and minimizing (#36859)
Add support to disable both window resizing and minimizing.

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

Release Notes:

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

Release Notes:

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

Release Notes:

- N/A

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

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

---------

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

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

---------

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

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

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


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

Release Notes:

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

Release Notes:

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

- N/A

---------

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

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

Release Notes:

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

- N/A

---------

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

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

- N/A

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

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

Closes #ISSUE

Release Notes:

- N/A
2025-08-27 00:24:56 -06:00
Caio Piccirillo
abd6009b41 Enhance syntax highlight for C++20 keywords (#36817)
Closes #36439 and #32999 

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

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

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

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

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

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

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

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

Release Notes:

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

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

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

Release Notes:

- N/A

---------

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

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

- N/A
2025-08-26 19:43:07 -03:00
Daniel Dye
d7c735959e Add xAI's Grok Code Fast 1 model (#36959)
Release Notes:

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

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-26 17:35:56 -03:00
Danilo Leal
bd4e943597 acp: Add onboarding modal & title bar banner (#36784)
Release Notes:

- N/A

---------

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

- N/A

---------

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

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

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


Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
2025-08-27 03:24:50 +08:00
Max Brunsfeld
b1b60bb7fe Work around duplicate ssh projects in workspace migration (#36946)
Fixes another case where the sqlite migration could fail, reported by
@SomeoneToIgnore.

Release Notes:

- N/A
2025-08-26 10:54:39 -07:00
Adam Mulvany
0e575b2809 helix: Fix buffer search: deploy reset to normal mode (#36917)
## Fix: Preserve Helix mode when using  search

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

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

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

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

### Settings

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

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

This is on Kubuntu 24.04.

The following test combinations pass locally:

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

Release Notes:

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

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

- N/A

---------

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

- N/A

---------

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

- N/A

---------

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

Release Notes:

- N/A

---------

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

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

- N/A
2025-08-26 11:05:40 +02:00
Max Brunsfeld
64b14ef848 Fix Sqlite newline syntax in workspace migration (#36916)
Fixes one more case where I incorrectly tried to use a `\n` escape
sequence for a newline in sqlite.

Release Notes:

- N/A
2025-08-25 22:21:05 -07:00
Rui Ning
bf5ed6d1c9 Remote: Change "sh -c" to "sh -lc" to make config in $HOME/.profile effective (#36760)
Closes #ISSUE

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

- N/A
2025-08-25 22:23:58 +00:00
Conrad Irwin
1460573dd4 acp: Rename dev command (#36908)
Release Notes:

- N/A
2025-08-25 16:04:44 -06:00
Kirill Bulatov
65de969cc8 Do not show directories in the InvalidBufferView (#36906)
Follow-up of https://github.com/zed-industries/zed/pull/36764

Release Notes:

- N/A
2025-08-25 21:16:37 +00:00
Danilo Leal
628a9cd8ea thread view: Add link to docs in the toolbar plus menu (#36883)
Release Notes:

- N/A
2025-08-25 17:34:55 -03:00
Gwen Lg
ad25aba990 remote_server: Improve error reporting (#33770)
Closes #33736

Use `thiserror` to implement error stack and `anyhow` to report is to
user.
Also move some code from main to remote_server to have better crate
isolation.

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-08-25 20:23:29 +00:00
Alvaro Parker
99cee8778c tab_switcher: Add support for diagnostics (#34547)
Support to show diagnostics on the tab switcher in the same way they are
displayed on the tab bar. This follows the setting
`tabs.show_diagnostics`.

This will improve user experience when disabling the tab bar and still
being able to see the diagnostics when switching tabs

Preview:

<img width="768" height="523" alt="Screenshot From 2025-07-16 11-02-42"
src="https://github.com/user-attachments/assets/308873ba-0458-485d-ae05-0de7c1cdfb28"
/>


Release Notes:

- Added diagnostics indicators to the tab switcher

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-08-25 20:18:03 +00:00
Cole Miller
823a0018e5 acp: Show output for read_file tool in a code block (#36900)
Release Notes:

- N/A
2025-08-25 20:10:17 +00:00
Conrad Irwin
9cc006ff74 acp: Update error matching (#36898)
Release Notes:

- N/A
2025-08-25 14:07:10 -06:00
Michael Sloan
0470baca50 open_ai: Remove model field from ResponseStreamEvent (#36902)
Closes #36901

Release Notes:

- Fixed use of Open WebUI as an LLM provider.
2025-08-25 19:50:08 +00:00
John Tur
4605b96630 Fix constant thread creation on Windows (#36779)
See
https://github.com/zed-industries/zed/issues/36057#issuecomment-3215808649

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

Release Notes:

- N/A
2025-08-25 12:45:28 -07:00
Danilo Leal
949398cb93 thread view: Fix some design papercuts (#36893)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Matt Miller <mattrx@gmail.com>
2025-08-25 21:07:30 +02:00
Cretezy
79e74b880b workspace: Allow disabling of padding on zoomed panels (#31913)
Screenshot:

| Before | After |
| -------|------|
|
![image](https://github.com/user-attachments/assets/629e7da2-6070-4abb-b469-3b0824524ca4)
|
![image](https://github.com/user-attachments/assets/99e54412-2e0b-4df9-9c40-a89b0411f6d8)
|
|
![image](https://github.com/user-attachments/assets/e99da846-f39b-47b5-808e-65c22a1af47b)
|
![image](https://github.com/user-attachments/assets/ccd4408f-8cce-44ec-a69a-81794125ec99)
|


Release Notes:

- Added `zoomed_padding` to allow disabling of padding around zoomed
panels

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2025-08-25 19:02:19 +00:00
Bennet Bo Fenner
59af2a7d1f acp: Add telemetry (#36894)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-25 20:51:23 +02:00
Danilo Leal
c786c0150f agent: Add section for agent servers in settings view (#35206)
Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-08-25 14:45:24 -04:00
Cole Miller
5fd29d37a6 acp: Model-specific prompt capabilities for 1PA (#36879)
Adds support for per-session prompt capabilities and capability changes
on the Zed side (ACP itself still only has per-connection static
capabilities for now), and uses it to reflect image support accurately
in 1PA threads based on the currently-selected model.

Release Notes:

- N/A
2025-08-25 14:28:11 -04:00
Mikayla Maki
f1204dfc33 Revert "workspace: Disable padding on zoomed panels" (#36884)
Reverts zed-industries/zed#36012

We thought we didn't need this UI, but it turns out it was load bearing
:)

Release Notes:

- Restored the zoomed panel padding
2025-08-25 10:46:36 -07:00
Marshall Bowers
2e1ca47241 Make fields of AiUpsellCard private (#36888)
This PR makes the fields of the `AiUpsellCard` private, for better
encapsulation.

Release Notes:

- N/A
2025-08-25 17:21:20 +00:00
Finn Evers
5c346a4ccf kotlin: Specify default language server (#36871)
As of
db52fc3655,
the Kotlin extension has two language servers. However, following that
change, no default language server for Kotlin was configured within this
repo, which led to two language servers being activated for Kotlin by
default.

This PR makes `kotlin-language-server` the default language server for
the extension. This also ensures that the [documentation within the
repository](https://github.com/zed-extensions/kotlin?tab=readme-ov-file#kotlin-lsp)
matches what is actually the case.


Release Notes:

- kotlin: Made `kotlin-language-server` the default language server.
2025-08-25 19:12:33 +02:00
Conrad Irwin
a102b08743 Require confirmation for fetch tool (#36881)
Using prompt injection, the agent may be tricked into making a fetch
request that includes unexpected data from the conversation in the URL.

As agent conversations may contain sensitive information (like private
code, or
potentially even API keys), this seems bad.

The easiest way to prevent this is to require the user to look at the
URL
before the model is allowed to fetch it.

Thanks to @ant4g0nist for bringing this to our attention.

Release Notes:

- agent panel: The fetch tool now requires confirmation.
2025-08-25 16:03:07 +00:00
Marshall Bowers
2dc4f156b3 Revert "Capture shorthand_field_initializer and modules in Rust highlights (#35842)" (#36880)
This PR reverts https://github.com/zed-industries/zed/pull/35842, as it
broke the syntax highlighting for `crate`:

### Before Revert

<img width="367" height="70" alt="Screenshot 2025-08-25 at 11 29 50 AM"
src="https://github.com/user-attachments/assets/ce9b8b59-4e89-43ed-84c7-95c0156b9168"
/>

### After Revert

<img width="353" height="69" alt="Screenshot 2025-08-25 at 11 32 17 AM"
src="https://github.com/user-attachments/assets/b6df5a21-64db-4abf-aa76-f085236da0c4"
/>

This reverts commit 896a35f7be.

Release Notes:

- Reverted https://github.com/zed-industries/zed/pull/35842.
2025-08-25 15:51:31 +00:00
Bennet Bo Fenner
557753d092 acp: Add Reauthenticate to dropdown (#36878)
Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-25 15:46:07 +00:00
Conrad Irwin
65fb17e2c9 acp: Remember following state (#36793)
A beta user reported that following was "lost" when asking for
confirmation, I
suspect they moved their cursor in the agent file while reviewing the
change.
Now we will resume following when the agent starts up again.

Release Notes:

- N/A
2025-08-25 09:34:30 -06:00
Smit Barmase
2fe3dbed31 project: Remove redundant Option from parse_register_capabilities (#36874)
Release Notes:

- N/A
2025-08-25 21:00:53 +05:30
Zach Riegel
fda5111dc0 Add CSS language injections for calls to styled (#33966)
…emotion).

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

Release Notes:

- Added CSS language injection support for styled-components and emotion
in JavaScript, TypeScript, and TSX files.
2025-08-25 11:30:09 -04:00
Antonio Scandurra
69127d2bea acp: Simplify control flow for native agent loop (#36868)
Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-25 13:38:19 +00:00
Bennet Bo Fenner
db949546cf agent2: Less noisy logs (#36863)
Release Notes:

- N/A
2025-08-25 15:14:48 +02:00
Danilo Leal
2b5a302972 thread view: Prevent user message controls to be cut-off (#36865)
In the thread view, when focusing on the user message, we display the
editing control container absolutely-positioned in the top right.
However, if there are no rules items and no restore checkpoint button
_and_ it is the very first message, the editing controls container would
be cut-off. This PR fixes that by giving it a bit more top padding.

Release Notes:

- N/A
2025-08-25 10:08:48 -03:00
Bennet Bo Fenner
4c0ad95acc acp: Show retry button for errors (#36862)
Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-25 12:52:25 +00:00
Cole Miller
8c83281399 acp: Fix read_file tool flickering (#36854)
We were rendering a Markdown link like `[Read file x.rs (lines
Y-Z)](@selection)` while the tool ran, but then switching to just `x.rs`
as soon as we got the file location from the tool call (due to an
if/else in the UI code that applies to all tools). This caused a
flicker, which is fixed by having `initial_title` return just the
filename from the input as it arrives instead of a link that we're going
to stop rendering almost immediately anyway.

Release Notes:

- N/A
2025-08-25 08:23:36 -04:00
Danilo Leal
dfc99de7b8 thread view: Add a few UI tweaks (#36845)
Release Notes:

- N/A
2025-08-25 08:18:23 -03:00
versecafe
fe5e81203f Fix macOS arch reporting from arch_ios to arch_arm (#36217)
```xml 
<key>arch_kind</key> 
<string>arch_arm</string> 
```

Closes #36037

Release Notes:

- N/A
2025-08-25 13:55:56 +03:00
Hendrik Müller
c48197b280 util: Fix edge case when parsing paths (#36025)
Searching for files broke a couple releases ago. It used to be possible
to start typing part of a file name, then select a file (not confirm it
yet) and then type in `:` and a line number to navigate directly to that
line.
The current behavior can be seen in the following screenshots. When the
`:` is typed, the selection is lost, since no files match any more.
<img width="552" height="370" alt="Screenshot From 2025-08-12 10-36-08"
src="https://github.com/user-attachments/assets/e4b4b613-7f0c-40d7-94c9-04d8ab541656"
/>
<img width="553" height="124" alt="Screenshot From 2025-08-12 10-36-25"
src="https://github.com/user-attachments/assets/843e9ecf-9e08-4fa6-9340-0388a957cbb2"
/>
<img width="549" height="370" alt="Screenshot From 2025-08-12 10-36-47"
src="https://github.com/user-attachments/assets/4a1bbbd8-268a-4ea8-999f-6cef1eb34a45"
/>

---

With this PR, the previous behavior is restored and can be seen in these
screenshots:

<img width="552" height="370" alt="Screenshot From 2025-08-12 10-36-08"
src="https://github.com/user-attachments/assets/466e1906-4735-47ae-a699-117bdd6490ca"
/>
<img width="549" height="370" alt="Screenshot From 2025-08-12 10-47-07"
src="https://github.com/user-attachments/assets/17f3acda-662d-4962-9eb8-4b494f211d26"
/>
<img width="549" height="370" alt="Screenshot From 2025-08-12 10-47-21"
src="https://github.com/user-attachments/assets/d98447fe-7377-4f4f-b3da-f690cd44c141"
/>

---

Release Notes:

- Adjusted the file finder to show matching file paths when adding the
`:row:column` to the query
2025-08-25 12:28:33 +03:00
Aleksei Gusev
11545c669e Add file icons to multibuffer view (#36836)
<img width="1988" height="1420" alt="multi-buffer-icons-git-diff"
src="https://github.com/user-attachments/assets/48f9722f-ca09-4aa7-ad7a-0b7e85f440d9"
/>

Unfortunately, `cargo format` decided to reformat everything. Probably,
because of hitting the right margin, no idea. The essence of this change
is the following:

```rust
.map(|path_header| {
    let filename = filename
        .map(SharedString::from)
        .unwrap_or_else(|| "untitled".into());
    let path = path::Path::new(filename.as_str());
    let icon =
        FileIcons::get_icon(path, cx).unwrap_or_default();
    let icon = Icon::from_path(icon).color(Color::Muted);

    let label = Label::new(filename).single_line().when_some(
        file_status,
        |el, status| {
            el.color(if status.is_conflicted() {
                Color::Conflict
            } else if status.is_modified() {
                Color::Modified
            } else if status.is_deleted() {
                Color::Disabled
            } else {
                Color::Created
            })
            .when(status.is_deleted(), |el| el.strikethrough())
        },
    );

    path_header.child(icon).child(label)
})
``` 

Release Notes:

- Added file icons to multi buffer view
2025-08-24 18:57:12 +02:00
Antonio Scandurra
a79aef7bdd acp: Never build a request with a tool use without its corresponding result (#36847)
Release Notes:

- N/A
2025-08-24 16:30:34 +00:00
Bennet Bo Fenner
d8bffd7ef2 acp: Cancel editing when focus is lost and message was not changed (#36822)
Release Notes:

- N/A
2025-08-24 11:05:39 +00:00
Chuqiao Feng
54c7d9dc5f Fix crash when opening inspector on Windows debug build (#36829) 2025-08-24 11:01:42 +00:00
tidely
dd6fce6d4e multi_buffer: Pre-allocate IDs when editing (#36819)
Something I came across when looking at `edit_internal`. Potentially
saves multiple re-allocations on an edit

Release Notes:

- N/A
2025-08-24 09:59:32 +03:00
versecafe
de5f87e8f2 languages: Add module to TS/JS keywords (#36830)
<img width="376" height="166" alt="image"
src="https://github.com/user-attachments/assets/ae32d74c-387b-4809-a0d6-cfa97888347d"
/>


Release Notes:

- Improved syntax highlights for `module` keyword in TS/JS
2025-08-24 09:54:47 +03:00
Cole Miller
1b91f3de41 acp: Fix accidentally reverted thread view changes (#36825)
Merge conflict resolution for #36741 accidentally reverted the changes
in #36670 to allow expanding terminals individually and in #36675 to
allow collapsing edit cards. This PR re-applies those changes, fixing
the regression.

Release Notes:

- N/A
2025-08-24 00:02:23 +00:00
Cole Miller
19764794b7 acp: Animate loading context creases (#36814)
- Add pulsating animation for context creases while they're loading
- Add spinner in message editors (replacing send button) during the
window where sending has been requested, but we haven't finished loading
the message contents to send to the model
- During the same window, ignore further send requests, so we don't end
up sending the same message twice if you mash enter while loading is in
progress
- Wait for context to load before rewinding the thread when sending an
edited past message, avoiding an empty-looking state during the same
window

Release Notes:

- N/A
2025-08-23 16:39:14 -04:00
itsaphel
d49409caba docs: Update settings in diagnostics.md (#36806)
For project_panel, the diagnostics key seems to be `show_diagnostics`
not `diagnostics`
([source](https://github.com/zed-industries/zed/blob/main/crates/project_panel/src/project_panel_settings.rs#L149-L152)).
Updating the docs accordingly

Release Notes:

- N/A
2025-08-23 19:11:27 +03:00
Smit Barmase
60ea4754b2 project: Fix dynamic registration for textDocument/documentColor (#36807)
From:
d90a87f955/protocol/src/common/protocol.colorProvider.ts (L50)

Release Notes:

- N/A
2025-08-23 20:30:16 +05:30
Antonio Scandurra
61bc1cc441 acp: Support launching custom agent servers (#36805)
It's enough to add this to your settings:

```json
{
    "agent_servers": {
        "Name Of Your Agent": {
            "command": "/path/to/custom/agent",
            "args": ["arguments", "that", "you", "want"],
        }
    }
}
```

Release Notes:

- N/A
2025-08-23 14:30:54 +00:00
Kirill Bulatov
70575d1115 Remove redundant Cargo diagnostics settings (#36795)
Removes `diagnostics.cargo.fetch_cargo_diagnostics` settings as those
are not needed for the flycheck diagnostics to run.
This setting disabled `checkOnSave` in rust-analyzer and allowed to
update diagnostics via flycheck in the project diagnostics editor with
the "refresh" button.

Instead, `"checkOnSave": false,` can be set manually as
https://zed.dev/docs/languages/rust#more-server-configuration example
shows and flycheck commands can be called manually from anywhere,
including the diagnostics panel, to refresh the diagnostics.

Release Notes:

- Removed redundant `diagnostics.cargo.fetch_cargo_diagnostics` settings
2025-08-23 07:03:36 +00:00
Cole Miller
ea42013746 acp: Eagerly load all kinds of mentions (#36741)
This PR makes it so that all kinds of @-mentions start loading their
context as soon as they are confirmed. Previously, we were waiting to
load the context for file, symbol, selection, and rule mentions until
the user's message was sent. By kicking off loading immediately for
these kinds of context, we can support adding selections from unsaved
buffers, and we make the semantics of @-mentions more consistent.

Loading all kinds of context eagerly also makes it possible to simplify
the structure of the MentionSet and the code around it. Now MentionSet
is just a single hash map, all the management of creases happens in a
uniform way in `MessageEditor::confirm_completion`, and the helper
methods for loading different kinds of context are much more focused and
orthogonal.

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2025-08-23 01:21:20 -04:00
Conrad Irwin
5da31fdb72 acp: Remove ACP v0 (#36785)
We had a few people confused about why some features weren't working due
to the fallback logic.

It's gone.

Release Notes:

- N/A
2025-08-22 22:09:08 -06:00
Danilo Leal
f48a8f2b6a thread view: Simplify tool call & improve required auth state UIs (#36783)
Release Notes:

- N/A
2025-08-22 20:10:26 -03:00
Kirill Bulatov
d24cad30f3 Be more lenient when dealing with rust-analyzer's flycheck commands (#36782)
Flycheck commands are global and makes sense to fall back to looking up
project's rust-analyzer even if the commands are run on a non-rust
buffer. If multiple rust-analyzers are found in the project, avoid
ambiguous commands and bail (as before).

Closes #ISSUE

Release Notes:

- Made it possible to run rust-analyzer's flycheck actions from anywhere
in the project
2025-08-22 22:55:50 +00:00
Max Brunsfeld
153724aad3 Clean up handling of serialized ssh connection ids (#36781)
Small follow-up to #36714

Release Notes:

- N/A
2025-08-22 15:44:58 -07:00
Bennet Bo Fenner
bc566fe18e agent2: Tweak usage callout border (#36777)
Release Notes:

- N/A
2025-08-22 22:35:26 +00:00
Mikayla Maki
91b2a84001 Add a few more testing features (#36778)
Release Notes:

- N/A

---------

Co-authored-by: Marshall <marshall@zed.dev>
2025-08-22 22:17:02 +00:00
Finn Evers
e6267c42f7 Ensure pane: swap item right does not panic (#36765)
This fixes a panic I randomly ran into whilst mistyping in the command
palette: I accidentally ran `pane: swap item right`in a state where no
items were opened in my active pane. We were checking for `index + 1 ==
self.items.len()` there when it really should be `>=`, as otherwise in
the case of no items this panics.

This PR fixes the bug, adds a test for both the panic as well as the
actions themselves (they were untested previously). Lastly (and mostly),
this also cleans up a bit around existing actions to update them with
how we generally handle actions now.

Release Notes:

- Fixed a panic that could occur with the `pane: swap item right`
action.
2025-08-22 23:28:55 +02:00
Max Brunsfeld
f649c31bf9 Restructure persistence of remote workspaces to make room for WSL and other non-ssh remote projects (#36714)
This is another pure refactor, to prepare for adding direct WSL support.

###  Todo

* [x] Represent `paths` in the same way for all workspaces, instead of
having a completely separate SSH representation
* [x] Adjust sqlite tables
    * [x] `ssh_projects` -> `ssh_connections` (drop paths)
    * [x] `workspaces.local_paths` -> `paths`
    * [x] remove duplicate path columns on `workspaces`
* [x] Add migrations for backward-compatibility

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-08-22 14:10:45 -07:00
Danilo Leal
639417c2bc thread_view: Adjust empty state and error displays (#36774)
Also changes the message editor placeholder depending on the agent.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-22 20:40:52 +00:00
Jonathan Andersson
896a35f7be Capture shorthand_field_initializer and modules in Rust highlights (#35842)
Currently shorthand field initializers are not captured the same way as
the full initializers, leading to awkward and mismatching highlighting.
This PR addresses this fact, in addition to capturing new highlights:
- Tags the `!` as part of a macro invocation.
- Tags the identifier part of a lifetime as `@lifetime`.
- Tag module definitions as a new capture group, `@module`.
- Shorthand initializers are now properly tagged as `@property`.

Here's what the current version of Zed looks like:

<img width="596" height="683" alt="image"
src="https://github.com/user-attachments/assets/c9e52d8e-03dc-426b-8545-4fe872b803e0"
/>

With the new highlighting applied:

<img width="596" height="683" alt="image"
src="https://github.com/user-attachments/assets/b7bd9391-9910-456b-8198-6871174d0f4f"
/>

Release Notes:

- Improved highlighting of Rust files, including new highlight groups
for modules and shorthand initializers.
2025-08-22 20:16:43 +00:00
Kirill Bulatov
4560d1ec58 Use a better message for the InvalidBufferView (#36770)
Follow-up of https://github.com/zed-industries/zed/pull/36764

Release Notes:

- N/A
2025-08-22 20:09:37 +00:00
Agus Zubiaga
18ac4ac5ef ACP debug tools pane (#36768)
Adds a new "acp: open debug tools" action that opens a new workspace
item with a log of ACP messages for the active connection.

Release Notes:

- N/A
2025-08-22 19:32:49 +00:00
Lukas Wirth
72bd248544 editor: Fix multi buffer header context menu not handling absolute paths (#36769)
Release Notes:

- N/A
2025-08-22 18:49:12 +00:00
Kirill Bulatov
42ae3301d0 Show file open error view instead of the modal (#36764)
Closes https://github.com/zed-industries/zed/issues/36672

Before:
either 
<img width="966" height="642" alt="image"
src="https://github.com/user-attachments/assets/7263ea3c-3d48-4f4d-be9e-16b24ca6f60b"
/>
(when opening from the project panel)

or

<img width="959" height="1019" alt="image"
src="https://github.com/user-attachments/assets/834041d4-f4d6-46db-b333-803169ec4803"
/>

(for the rest of the cases)

After:

<img width="2032" height="1167" alt="Screenshot 2025-08-22 at 19 34 10"
src="https://github.com/user-attachments/assets/1aa4530b-69f6-4c3a-8ea1-d4035dbb28da"
/>

(the unified error view)

Release Notes:

- Improved unsupported file opening in Zed

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-08-22 17:04:39 +00:00
Oleksiy Syvokon
eb0f9ddcdc themes: Implement Bright Black and Bright White colors (#36761)
Before:
<img width="356" height="50" alt="image"
src="https://github.com/user-attachments/assets/c4f4ae53-8820-4f22-b306-2e5062cfe552"
/>

After:
<img width="340" height="41" alt="image"
src="https://github.com/user-attachments/assets/8e69d9dc-5640-4e41-845d-f299fc5954e3"
/>


Release Notes:

- Fixed ANSI Bright Black and Bright White colors
2025-08-22 16:03:47 +00:00
Peter Tripp
ac9fdaa1da onboarding: Improve Windows/Linux keyboard shortcuts; example ligature (#36712)
Small fixes to onboarding.
Correct ligature example.
Replace`ctrl-escape` and `alt-tab` since they are reserved on windows
(and often on linux) and so are caught by the OS.

Release Notes:

- N/A
2025-08-22 11:51:01 -04:00
Anthony Eid
8204ef1e51 onboarding: Remove accept AI ToS from within Zed (#36612)
Users now accept ToS from Zed's website when they sign in to Zed the
first time. So it's no longer possible that a signed in account could
not have accepted the ToS.


Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-08-22 11:45:47 -04:00
Kirill Bulatov
3d2fa72d1f Make word completions less intrusive (#36745)
Introduce `min_words_query_len` threshold for automatic word completion
display, and set it to 3 by default.

Re-enable word completions in Markdown and Plaintext.

Release Notes:

- Introduced `min_words_query_len` threshold for automatic word
completion display, and set it to 3 by default to make them less
intrusive
2025-08-22 13:58:17 +00:00
Piotr Osiewicz
92bbcdeb7d workspace: Do not prompt for hanging up current call when replacing last visible project (#36697)
This fixes a bug where in order to open a new project in a call (even if
it's not shared), you need to hang up.


Release Notes:

- N/A
2025-08-22 13:34:55 +00:00
Sarah Price
54df43e06f Fix cursor movement in protected files on backspace/delete (#36753)
## Summary

Fixes cursor movement behavior in protected files (like Default
Settings) when pressing backspace or delete keys.

Previously, these keys would cause unwanted cursor movement instead of
being ignored as expected in read-only files.

## Changes

- Added read-only checks to `backspace()` and `delete()` methods in the
editor
- Consistent with existing pattern used by other editing methods
(`indent()`, `outdent()`, `undo()`, etc.)

## Test Plan

1. Open Default Settings in Zed
2. Place cursor at arbitrary position (not at start/end of file)  
3. Press backspace - cursor should remain in place (no movement)
4. Press delete - cursor should remain in place (no movement)

Fixes #36302

Release Notes:

- Fixed backspace and delete keys moving caret in protected files

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-22 13:18:46 +00:00
Antonio Scandurra
4f0fad6996 acp: Support calling tools provided by MCP servers (#36752)
Release Notes:

- N/A
2025-08-22 13:16:42 +00:00
Danilo Leal
3b7c1744b4 thread view: Add more UI improvements (#36750)
Release Notes:

- N/A
2025-08-22 09:52:44 -03:00
Danilo Leal
27a26d53b1 thread view: Inform when editing previous messages is unavailable (#36727)
Release Notes:

- N/A
2025-08-22 08:28:03 -03:00
Cole Miller
d88fd00e87 acp: Fix panic with edit file tool (#36732)
We had a frequent panic when the agent was using our edit file tool. The
root cause was that we were constructing a `BufferDiff` with
`BufferDiff::new`, then calling `set_base_text`, but not waiting for
that asynchronous operation to finish. This means there was a window of
time where the diff's base text was set to the initial value of
`""`--that's not a problem in itself, but it was possible for us to call
`PendingDiff::update` during that window, which calls
`BufferDiff::update_diff`, which calls
`BufferDiffSnapshot::new_with_base_buffer`, which takes two arguments
`base_text` and `base_text_snapshot` that are supposed to represent the
same text. We were getting the first of those arguments from the
`base_text` field of `PendingDiff`, which is set immediately to the
target base text without waiting for `BufferDiff::set_base_text` to run
to completion; and the second from the `BufferDiff` itself, which still
has the empty base text during that window.

As a result of that mismatch, we could end up adding `DeletedHunk` diff
transforms to the multibuffer for the diff card even though the
multibuffer's base text was empty, ultimately leading to a panic very
far away in rendering code.

I've fixed this by adding a new `BufferDiff` constructor for the case
where the buffer contents and the base text are (initially) the same,
like for the diff cards, and so we don't need an async diff calculation.
I also added a debug assertion to catch the basic issue here earlier,
when `BufferDiffSnapshot::new_with_base_buffer` is called with two base
texts that don't match.

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2025-08-22 03:48:47 -04:00
Conrad Irwin
f4ba7997a7 acp: Fix history search (#36734)
Release Notes:

- N/A
2025-08-22 05:57:30 +00:00
Anthony Eid
e360691106 telemetry: Add panel button clicked event (#36735)
The event has two fields

1. name: The name of the panel being clicked
2. toggle_state: true if clicking to open, otherwise false

cc @katie-z-geer 

Release Notes:

- N/A
2025-08-22 05:54:25 +00:00
Anthony Eid
b349a8f34c ai: Auto select user model when there's no default (#36722)
This PR identifies automatic configuration options that users can select
from the agent panel. If no default provider is set in their settings,
the PR defaults to the first recommended option. Additionally, it
updates the selected provider for a thread when a user changes the
default provider through the settings file, if the thread hasn't had any
queries yet.

Release Notes:

- agent: automatically select a language model provider if there's no
user set provider.

---------

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-08-22 01:12:12 -04:00
Smit Barmase
e15856a37f Move APCA contrast from terminal_view to ui utils (#36731)
In prep for using this in the editor search/select highlighting. 

Release Notes:

- N/A
2025-08-22 10:17:37 +05:30
Adam Mulvany
852439452c vim: Fix cursor jumping past empty lines with inlay hints in visual mode (#35757)
**Summary**

Fixes #29134 - Visual mode cursor incorrectly jumps past empty lines
that contain inlay hints (type hints).

**Problem**

When in VIM visual mode, pressing j to move down from a longer line to
an empty line that contains an inlay hint would cause the cursor to skip
the empty line entirely and jump to the next line. This only occurred
when moving down (not up) and only in visual mode.

**Root Cause**

The issue was introduced by commit f9ee28db5e which added bias-based
navigation for handling multi-line inlay hints. When using Bias::Right
while moving down, the clipping logic would place the cursor past the
inlay hint, causing it to jump to the next line.

**Solution**
Added logic in up_down_buffer_rows to detect when clipping would place
the cursor within an inlay hint position. When detected, it uses the
buffer column position instead of the display column to avoid jumping
past the hint.

**Testing**

- Added comprehensive test case
test_visual_mode_with_inlay_hints_on_empty_line that reproduces the
exact scenario
- Manually verified the fix with the reproduction case from the issue
- All 356 tests pass with `cargo test -p vim`

**Release Notes:**
- Fixed VIM visual mode cursor jumping past empty lines with type hints
when navigating down
2025-08-21 21:20:22 -06:00
Kaem
f5fd4ac670 vim: Implement partial increment/decrement for visual selection (#36553)
This change adds the ability to increment / decrement numbers that are
part of a visual selection. Previously Zed would resolve to the entire
number under visual selection for increment as oppposed to only
incrementing the part of the number that is selected

Release Notes: 

- vim: Fixed increment/decrement in visual mode
2025-08-22 03:02:47 +00:00
Ben Brandt
e1a96b68f0 acp: Tool name prep (#36726)
Prep work for deduping tool names

Release Notes:

- N/A
2025-08-22 00:37:41 +00:00
Ben Kunkle
ca139b701e keymap_ui: Improve conflict resolution for semantically equal contexts (#36204)
Closes #ISSUE

Creates a function named `normalized_ctx_eq` that compares
`gpui::KeybindContextPredicate`'s while taking into account the
associativity of the binary operators. This function is now used to
compare context predicates in the keymap editor, greatly improving the
number of cases caught by our overloading and conflict detection

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-08-22 00:18:25 +00:00
Ben Kunkle
eeaadc098f Add GPU info to Sentry crashes (#36624)
Closes #ISSUE

Adds system GPU collection to crash reporting. Currently this is Linux
only.

The system GPUs are determined by reading the `/sys/class/drm` directory
structure, rather than using the exisiting `gpui::Window::gpu_specs()`
method in order to gather more information, and so that the GPU context
is not dependent on Vulkan context initialization (i.e. we still get GPU
info when Zed fails to start because Vulkan failed to initialize).

Unfortunately, the `blade` APIs do not support querying which GPU _will_
be used, so we do not know which GPU was attempted to be used when
Vulkan context initialization fails, however, when Vulkan initialization
succeeds, we send a message to the crash handler containing the result
of `gpui::Window::gpu_specs()` to include the "Active" gpu in any crash
report that may occur

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-08-21 19:59:42 -04:00
Danilo Leal
18fe68d991 thread view: Add small refinements to tool call UI (#36723)
Release Notes:

- N/A
2025-08-21 20:51:36 -03:00
Peter Tripp
a977fbc5b0 Document project_panel.sticky_scroll (#36721)
Hat tip to: @watercubz in
https://github.com/zed-industries/zed/issues/22869#issuecomment-3183850576

Release Notes:

- N/A
2025-08-21 18:40:07 -04:00
David Kleingeld
06c0e59379 Make tab switcher show preview of selected tab (#36718)
Similar to nvim's telescope this makes it easier to find the right tab
in the list.

The preview takes place in the pane where the tab resides.
- on dismiss: We restore all panes.
- on confirm: We restore all panes except the one where the selected tab
resides. For this reason we collect the active item for each pane before
the tabswither starts.

Release Notes:

- Improved tab switcher, it now shows a preview of the selected tab

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-08-22 00:21:36 +02:00
Agus Zubiaga
0beb919bbb acp: Fix MessageEditor::set_message for sent messages (#36715)
The `PromptCapabilities` introduced in previous PRs were only getting
set on the main message editor and not for the editors in user messages.
This caused a bug where mentions would disappear after resending the
message, and for the completion provider to be limited to files.

Release Notes:

- N/A
2025-08-21 17:29:53 -03:00
Julia Ryan
20a0c3e920 Disable minidump generation on dev builds (again) (#36716)
We accidentally deleted this in #36267

Release Notes:

- N/A
2025-08-21 20:27:09 +00:00
Antonio Scandurra
731b5d0def acp: Allow editing of thread titles in agent2 (#36706)
Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-08-21 20:24:13 +00:00
Danilo Leal
555692fac6 thread view: Add improvements to the UI (#36680)
Release Notes:

- N/A
2025-08-21 17:05:29 -03:00
Agus Zubiaga
2234f91b7b acp: Remove invalid creases on edit (#36708)
Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-21 16:56:40 -03:00
Agus Zubiaga
725ed5dd01 acp: Hide loading diff animation for external agents and update in place (#36699)
The loading diff animation can be jarring for external agents because
they stream the diff at the same time the tool call is pushed, so it's
only displayed while we're asynchronously calculating the diff. We'll
now only show it for the native agent.

Also, we'll now only update the diff when it changes, which avoids
unnecessarily hiding it for a few frames.

Release Notes:

- N/A

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-08-21 16:56:15 -03:00
Ben Brandt
d0583ede48 acp: Move ignored integration tests behind e2e flag (#36711)
Release Notes:

- N/A
2025-08-21 19:06:27 +00:00
Lukas Wirth
33e05f15b2 collab_ui: Fix channel text bleeding through buttons on hover (#36710)
Release Notes:

- N/A
2025-08-21 18:50:06 +00:00
Dave Waggoner
c1e749906f Add terminal view path like target tests (#35422)
Part of 
- #28238

This PR refactors `Event::NewNavigationTarget` and `Event::Open`
handling of `PathLikeTarget` and associated code in `terminal_view.rs`
into its own file, `terminal_path_like_target.rs` for improved
testability, and adds tests which cover cases from:
  - #28339
  - #28407
  - #33498 
  - #34027
  - #34078

Release Notes:

- N/A
2025-08-21 13:41:32 -05:00
Vitaly Slobodin
81cb24810b ruby: Improve Ruby test and debug task configurations (#36691)
Hi! This pull request adds missing `cwd` field to all Ruby test tasks
otherwise `rdbg` will be broken when the user tries to debug a test.
Thanks!

Release Notes:

- N/A
2025-08-21 20:23:41 +03:00
Peter Tripp
f2899bf34b ci: Switch from ubuntu-latest to namespace (2) (#36702)
In response to ongoing [github actions
incident](https://www.githubstatus.com/incidents/c7kq3ctclddp)

Supercedes: https://github.com/zed-industries/zed/pull/36698

Release Notes:

- N/A
2025-08-21 13:21:37 -04:00
Julia Ryan
1b2ceae7ef Use Tokio::spawn instead of getting an executor handle (#36701)
This was causing panics due to the handles being dropped out of order.
It doesn't seem possible to guarantee the correct drop ordering given
that we're holding them over await points, so lets just spawn on the
tokio executor itself which gives us access to the state we needed those
handles for in the first place.

Fixes: ZED-1R

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-08-21 17:19:57 +00:00
Peter Tripp
d166ab95a1 ci: Switch Windows jobs to target explicit tag (#36693)
The previous tags are non-customizable (added by default).
This will enable us to pull specific runs out of the pool for
maintenance.

Also disable actionlint invoking shellcheck because it chokes on
PowerShell.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
2025-08-21 13:09:14 -04:00
Lukas Wirth
b284b1a0b8 remote: Fetch shell on ssh remote to use for preparing commands (#36690)
Prerequisite for https://github.com/zed-industries/zed/pull/36576 to
allow us to differentiate the shell in a remote.

Release Notes:

- N/A
2025-08-21 19:08:26 +02:00
Julia Ryan
6f32d36ec9 Upload telemetry event on crashes (#36695)
This will let us track crashes-per-launch using the new minidump-based
crash reporting.

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-08-21 17:03:30 +00:00
Antonio Scandurra
190217a43b acp: Refactor agent2 send to have a clearer control flow (#36689)
Release Notes:

- N/A
2025-08-21 18:11:05 +02:00
Piotr Osiewicz
132daef9f6 lsp: Add basic test for server tree toolchain use (#36692)
Closes #ISSUE

Release Notes:

- N/A
2025-08-21 17:52:17 +02:00
Agus Zubiaga
4bee06e507 acp: Use ResourceLink for agents that don't support embedded context (#36687)
The completion provider was already limiting the mention kinds according
to `acp::PromptCapabilities`. However, it was still using
`ContentBlock::EmbeddedResource` when
`acp::PromptCapabilities::embedded_context` was `false`. We will now use
`ResourceLink` in that case making it more complaint with the
specification.

Release Notes:

- N/A
2025-08-21 14:57:46 +00:00
Ryan Drew
f23314bef4 editor: Use editorconfig's max_line_length for hard wrap (#36426)
PR #20198, "Do not alter soft wrap based on .editorconfig contents"
removed support for setting line lengths for both soft and hard wrap,
not just soft wrap. This causes the `max_line_length` property within a
`.editorconfig` file to be ignored by Zed. This commit restores allowing
for hard wrap limits to be set using `max_line_length` without impacting
soft wrap limits. This is done by merging the `max_line_length` property
from an editorconfig file into Zed's `preferred_line_length` property.

Release Notes:

- Added support for .editorconfig's `max_line_length` property

Signed-off-by: Ryan Drew <git@ry4n.me>
2025-08-21 17:55:43 +03:00
Smit Barmase
697a39c251 Fix issue where renaming a file would not update imports in related files if they are not open (#36681)
Closes #34445

Now we open a multi-buffer consisting of buffers that have updated,
renamed file imports.

Only local is handled, for now.

Release Notes:

- Fixed an issue where renaming a file would not update imports in
related files if they are not already open.
2025-08-21 20:19:17 +05:30
Conrad Irwin
d9ea97ee9c acp: Detect gemini auth errors and show a button (#36641)
Closes #ISSUE

Release Notes:

- N/A
2025-08-21 08:44:04 -06:00
Conrad Irwin
d8fc779a67 acp: Hide history unless in native agent (#36644)
Release Notes:

- N/A
2025-08-21 08:43:57 -06:00
Bennet Bo Fenner
001ec97c0e acp: Use file icons for edit tool cards when ToolCallLocation is known (#36684)
Release Notes:

- N/A
2025-08-21 14:18:22 +00:00
Marshall Bowers
2781a30971 collab: Add Orb subscription status and period to billing_subscriptions table (#36682)
This PR adds the following new columns to the `billing_subscriptions`
table:

- `orb_subscription_status`
- `orb_current_billing_period_start_date`
- `orb_current_billing_period_end_date`

Release Notes:

- N/A
2025-08-21 13:59:18 +00:00
David Kleingeld
e0613cbd0f Add Rodio audio pipeline as alternative to current LiveKit pipeline (#36607)
Rodio parts are well tested and need less configuration then the livekit
parts. I suspect there is a bug in the livekit configuration regarding
resampling. Rather then investigate that it seemed faster & easier to
swap in Rodio.

This opens the door to using other Rodio parts like:
 - Decibel based volume control
 - Limiter (prevents sound from becoming too loud)
 - Automatic gain control

To use this add to settings:
```
  "audio": {
    "experimental.rodio_audio": true
  }
```

Release Notes:

- N/A

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-08-21 15:56:16 +02:00
Cole Miller
1dd237139c Fix more improper uses of the buffer_id field of Anchor (#36636)
Follow-up to #36524 

Release Notes:

- N/A
2025-08-21 09:24:34 -04:00
Cole Miller
f63d8e4c53 Show excerpt dividers in without_headers multibuffers (#36647)
Release Notes:

- Fixed diff cards in agent threads not showing dividers between
disjoint edited regions.
2025-08-21 13:23:56 +00:00
Bennet Bo Fenner
ad64a71f04 acp: Allow collapsing edit file tool calls (#36675)
Release Notes:

- N/A
2025-08-21 11:05:41 +00:00
Antonio Scandurra
f435af2fde acp: Use unstaged style for diffs (#36674)
Release Notes:

- N/A
2025-08-21 10:59:51 +00:00
Julia Ryan
c5ee3f3e2e Avoid suspending panicking thread while crashing (#36645)
On the latest build @maxbrunsfeld got a panic that hung zed. It appeared
that the hang occured after the minidump had been successfully written,
so our theory on what happened is that the `suspend_all_other_threads`
call in the crash handler suspended the panicking thread (due to the
signal from simulate_exception being received on a different thread),
and then when the crash handler returned everything was suspended so the
panic hook never made it to the `process::abort`.

This change makes the crash handler avoid _both_ the current and the
panicking thread which should avoid that scenario.

Release Notes:

- N/A
2025-08-21 10:33:45 +00:00
Piotr Osiewicz
7f1bd2f15e remote: Fix toolchain RPC messages not being handled because of the entity getting dropped (#36665)
Release Notes:

- N/A
2025-08-21 09:37:45 +00:00
Bennet Bo Fenner
62f2ef86dc agent2: Allow expanding terminals individually (#36670)
Release Notes:

- N/A
2025-08-21 11:25:00 +02:00
Antonio Scandurra
fda6eda3c2 Fix @-mentioning threads when their summary isn't ready yet (#36664)
Release Notes:

- N/A
2025-08-21 08:57:28 +00:00
Kirill Bulatov
ed84767c9d Fix overlooked Clippy lints (#36659)
Follow-up of https://github.com/zed-industries/zed/pull/36557 that is
needed after https://github.com/zed-industries/zed/pull/36652

Release Notes:

- N/A
2025-08-21 06:48:04 +00:00
Kirill Bulatov
cde0a5dd27 Add a non-style lint exclusion (#36658)
Follow-up of https://github.com/zed-industries/zed/pull/36651
Restores https://github.com/zed-industries/zed/pull/35955 footgun guard.

Release Notes:

- N/A
2025-08-21 06:36:57 +00:00
Sachith Shetty
68f97d6069 editor: Use highlight_text to highlight matching brackets, fix unnecessary inlay hint highlighting (#36540)
Closes #35981

Release Notes:

- Fixed bracket highlights overly including parts of inlays when
highlighting

Before -
<img width="1480" height="602" alt="Screenshot from 2025-08-19 17-15-06"
src="https://github.com/user-attachments/assets/8e6b5ed8-f133-4867-8352-ed93441fbd8b"
/>

After -
<img width="1480" height="602" alt="Screenshot from 2025-08-19 17-24-26"
src="https://github.com/user-attachments/assets/1314e54e-ecf9-4280-9d53-eed6e96e393f"
/>
2025-08-21 09:27:41 +03:00
Kirill Bulatov
5dcb90858e Stop waiting for part of LSP responses on remote Collab clients' part (#36557)
Instead of holding a connection for potentially long LSP queries (e.g.
rust-analyzer might take minutes to look up a definition), disconnect
right after sending the initial request and handle the follow-up
responses later.

As a bonus, this allows to cancel previously sent request on the local
Collab clients' side due to this, as instead of holding and serving the
old connection, local clients now can stop previous requests, if needed.

Current PR does not convert all LSP requests to the new paradigm, but
the problematic ones, deprecating `MultiLspQuery` and moving all its
requests to the new paradigm.

Release Notes:

- Improved resource usage when querying LSP over Collab

---------

Co-authored-by: David Kleingeld <git@davidsk.dev>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-21 09:24:34 +03:00
Conrad Irwin
c731bb6d91 Re-add redundant clone (#36652)
Although I said I'd do this, I actually didn't...

Updates #36651

Release Notes:

- N/A
2025-08-20 21:08:49 -06:00
Conrad Irwin
4b03d791b5 Remove style lints for now (#36651)
Closes #36577

Release Notes:

- N/A
2025-08-20 20:38:30 -06:00
Agus Zubiaga
9a3e4c47d0 acp: Suggest upgrading to preview instead of latest (#36648)
A previous PR changed the install command from `@latest` to `@preview`,
but the upgrade command kept suggesting `@latest`.

Release Notes:

- N/A
2025-08-21 00:52:38 +00:00
Ben Brandt
568e1d0a42 acp: Add e2e test support for NativeAgent (#36635)
Release Notes:

- N/A
2025-08-21 00:36:50 +00:00
Agus Zubiaga
6f242772cc acp: Update to 0.0.30 (#36643)
See: https://github.com/zed-industries/agent-client-protocol/pull/20

Release Notes:

- N/A
2025-08-21 00:10:36 +00:00
张小白
8ef9ecc91f windows: Fix RevealInFileManager (#36592)
Closes #36314

This PR takes inspiration from [Electron’s
implementation](dd54e84a58/shell/common/platform_util_win.cc (L268-L314)).

Before and after:



https://github.com/user-attachments/assets/53eec5d3-23c7-4ee1-8477-e524b0538f60



Release Notes:

- N/A
2025-08-21 08:08:54 +08:00
Ben Kunkle
3dd362978a docs: Add table of all actions (#36642)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-08-20 23:41:06 +00:00
Agus Zubiaga
74c0ba980b acp: Reliably suppress gemini abort error (#36640)
https://github.com/zed-industries/zed/pull/36633 relied on the prompt
request responding before cancel, but that's not guaranteed


Release Notes:

- N/A
2025-08-20 23:32:17 +00:00
Marshall Bowers
c20233e0b4 agent_ui: Fix signed-in check in Zed provider configuration (#36639)
This PR fixes the check for if the user is signed in in the Agent panel
configuration.

Supersedes https://github.com/zed-industries/zed/pull/36634.

Release Notes:

- Fixed the user's plan badge near the Zed provider in the Agent panel
not showing despite being signed in.
2025-08-20 23:09:09 +00:00
Agus Zubiaga
ffb995181e acp: Supress gemini aborted errors (#36633)
This PR adds a temporary workaround to supress "Aborted" errors from
Gemini when cancelling generation. This won't be needed once
https://github.com/google-gemini/gemini-cli/pull/6656 is generally
available.

Release Notes:

- N/A
2025-08-20 22:30:25 +00:00
Conrad Irwin
5120b6b7f9 acp: Handle Gemini Auth Better (#36631)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-08-20 16:12:41 -06:00
Julia Ryan
c9c708ff08 nix: Re-enable nightly builds (#36632)
Release Notes:

- N/A
2025-08-20 21:43:53 +00:00
Agus Zubiaga
9e34bb3f05 acp: Hide feedback buttons for external agents (#36630)
Release Notes:

- N/A
2025-08-20 21:35:48 +00:00
Cole Miller
595cf1c6c3 acp: Rename assistant::QuoteSelection and support it in agent2 threads (#36628)
Release Notes:

- N/A
2025-08-20 21:31:25 +00:00
Agus Zubiaga
d1820b183a acp: Suggest installing gemini@preview instead of latest (#36629)
Release Notes:

- N/A
2025-08-20 21:26:07 +00:00
Danilo Leal
fb7edbfb46 thread_view: Add recent history entries & adjust empty state (#36625)
Release Notes:

- N/A
2025-08-20 18:01:22 -03:00
Agus Zubiaga
02dabbb9fa acp thread view: Do not go into editing mode if unsupported (#36623)
Release Notes:

- N/A
2025-08-20 20:05:53 +00:00
Joseph T. Lyons
fa8bef1496 Bump Zed to v0.202 (#36622)
Release Notes:

-N/A
2025-08-20 20:05:30 +00:00
Cole Miller
739e4551da Fix typo in Excerpt::contains (#36621)
Follow-up to #36524 

Release Notes:

- N/A
2025-08-20 19:30:11 +00:00
Ben Brandt
b0bef3a9a2 agent2: Clean up tool descriptions (#36619)
schemars was passing along the newlines from the doc comments. This
should make these closer to the markdown file versions we had in the old
agent.

Release Notes:

- N/A
2025-08-20 19:17:07 +00:00
600 changed files with 36008 additions and 16819 deletions

View File

@@ -41,5 +41,4 @@ workspace-members = [
"slash_commands_example",
"zed_snippets",
"zed_test_extension",
"zed_toml",
]

3
.gitattributes vendored
View File

@@ -1,2 +1,5 @@
# Prevent GitHub from displaying comments within JSON files as errors.
*.json linguist-language=JSON-with-Comments
# Ensure the WSL script always has LF line endings, even on Windows
crates/zed/resources/windows/zed-wsl text eol=lf

View File

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

View File

@@ -19,14 +19,27 @@ self-hosted-runner:
- namespace-profile-16x32-ubuntu-2004-arm
- namespace-profile-32x64-ubuntu-2004-arm
# Namespace Ubuntu 22.04 (Everything else)
- namespace-profile-2x4-ubuntu-2204
- namespace-profile-4x8-ubuntu-2204
- namespace-profile-8x16-ubuntu-2204
- namespace-profile-16x32-ubuntu-2204
- namespace-profile-32x64-ubuntu-2204
# Namespace Ubuntu 24.04 (like ubuntu-latest)
- namespace-profile-2x4-ubuntu-2404
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4
# Self Hosted Runners
- self-mini-macos
- self-32vcpu-windows-2022
# Disable shellcheck because it doesn't like powershell
# This should have been triggered with initial rollout of actionlint
# but https://github.com/zed-industries/zed/pull/36693
# somehow caused actionlint to actually check those windows jobs
# where previously they were being skipped. Likely caused by an
# unknown bug in actionlint where parsing of `runs-on: [ ]`
# breaks something else. (yuck)
paths:
.github/workflows/{ci,release_nightly}.yml:
ignore:
- "shellcheck"

View File

@@ -20,167 +20,8 @@ runs:
with:
node-version: "18"
- name: Configure crash dumps
shell: powershell
run: |
# Record the start time for this CI run
$runStartTime = Get-Date
$runStartTimeStr = $runStartTime.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "CI run started at: $runStartTimeStr"
# Save the timestamp for later use
echo "CI_RUN_START_TIME=$($runStartTime.Ticks)" >> $env:GITHUB_ENV
# Create crash dump directory in workspace (non-persistent)
$dumpPath = "$env:GITHUB_WORKSPACE\crash_dumps"
New-Item -ItemType Directory -Force -Path $dumpPath | Out-Null
Write-Host "Setting up crash dump detection..."
Write-Host "Workspace dump path: $dumpPath"
# Note: We're NOT modifying registry on stateful runners
# Instead, we'll check default Windows crash locations after tests
- name: Run tests
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
$env:RUST_BACKTRACE = "full"
# Enable Windows debugging features
$env:_NT_SYMBOL_PATH = "srv*https://msdl.microsoft.com/download/symbols"
# .NET crash dump environment variables (ephemeral)
$env:COMPlus_DbgEnableMiniDump = "1"
$env:COMPlus_DbgMiniDumpType = "4"
$env:COMPlus_CreateDumpDiagnostics = "1"
cargo nextest run --workspace --no-fail-fast
- name: Analyze crash dumps
if: always()
shell: powershell
run: |
Write-Host "Checking for crash dumps..."
# Get the CI run start time from the environment
$runStartTime = [DateTime]::new([long]$env:CI_RUN_START_TIME)
Write-Host "Only analyzing dumps created after: $($runStartTime.ToString('yyyy-MM-dd HH:mm:ss'))"
# Check all possible crash dump locations
$searchPaths = @(
"$env:GITHUB_WORKSPACE\crash_dumps",
"$env:LOCALAPPDATA\CrashDumps",
"$env:TEMP",
"$env:GITHUB_WORKSPACE",
"$env:USERPROFILE\AppData\Local\CrashDumps",
"C:\Windows\System32\config\systemprofile\AppData\Local\CrashDumps"
)
$dumps = @()
foreach ($path in $searchPaths) {
if (Test-Path $path) {
Write-Host "Searching in: $path"
$found = Get-ChildItem "$path\*.dmp" -ErrorAction SilentlyContinue | Where-Object {
$_.CreationTime -gt $runStartTime
}
if ($found) {
$dumps += $found
Write-Host " Found $($found.Count) dump(s) from this CI run"
}
}
}
if ($dumps) {
Write-Host "Found $($dumps.Count) crash dump(s)"
# Install debugging tools if not present
$cdbPath = "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe"
if (-not (Test-Path $cdbPath)) {
Write-Host "Installing Windows Debugging Tools..."
$url = "https://go.microsoft.com/fwlink/?linkid=2237387"
Invoke-WebRequest -Uri $url -OutFile winsdksetup.exe
Start-Process -Wait winsdksetup.exe -ArgumentList "/features OptionId.WindowsDesktopDebuggers /quiet"
}
foreach ($dump in $dumps) {
Write-Host "`n=================================="
Write-Host "Analyzing crash dump: $($dump.Name)"
Write-Host "Size: $([math]::Round($dump.Length / 1MB, 2)) MB"
Write-Host "Time: $($dump.CreationTime)"
Write-Host "=================================="
# Set symbol path
$env:_NT_SYMBOL_PATH = "srv*C:\symbols*https://msdl.microsoft.com/download/symbols"
# Run analysis
$analysisOutput = & $cdbPath -z $dump.FullName -c "!analyze -v; ~*k; lm; q" 2>&1 | Out-String
# Extract key information
if ($analysisOutput -match "ExceptionCode:\s*([\w]+)") {
Write-Host "Exception Code: $($Matches[1])"
if ($Matches[1] -eq "c0000005") {
Write-Host "Exception Type: ACCESS VIOLATION"
}
}
if ($analysisOutput -match "EXCEPTION_RECORD:\s*(.+)") {
Write-Host "Exception Record: $($Matches[1])"
}
if ($analysisOutput -match "FAULTING_IP:\s*\n(.+)") {
Write-Host "Faulting Instruction: $($Matches[1])"
}
# Save full analysis
$analysisFile = "$($dump.FullName).analysis.txt"
$analysisOutput | Out-File -FilePath $analysisFile
Write-Host "`nFull analysis saved to: $analysisFile"
# Print stack trace section
Write-Host "`n--- Stack Trace Preview ---"
$stackSection = $analysisOutput -split "STACK_TEXT:" | Select-Object -Last 1
$stackLines = $stackSection -split "`n" | Select-Object -First 20
$stackLines | ForEach-Object { Write-Host $_ }
Write-Host "--- End Stack Trace Preview ---"
}
Write-Host "`n⚠ Crash dumps detected! Download the 'crash-dumps' artifact for detailed analysis."
# Copy dumps to workspace for artifact upload
$artifactPath = "$env:GITHUB_WORKSPACE\crash_dumps_collected"
New-Item -ItemType Directory -Force -Path $artifactPath | Out-Null
foreach ($dump in $dumps) {
$destName = "$($dump.Directory.Name)_$($dump.Name)"
Copy-Item $dump.FullName -Destination "$artifactPath\$destName"
if (Test-Path "$($dump.FullName).analysis.txt") {
Copy-Item "$($dump.FullName).analysis.txt" -Destination "$artifactPath\$destName.analysis.txt"
}
}
Write-Host "Copied $($dumps.Count) dump(s) to artifact directory"
} else {
Write-Host "No crash dumps from this CI run found"
}
- name: Upload crash dumps
if: always()
uses: actions/upload-artifact@v4
with:
name: crash-dumps-${{ github.run_id }}-${{ github.run_attempt }}
path: |
crash_dumps_collected/*.dmp
crash_dumps_collected/*.txt
if-no-files-found: ignore
retention-days: 7
- name: Check test results
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: |
# Re-check test results to fail the job if tests failed
if ($LASTEXITCODE -ne 0) {
Write-Host "Tests failed with exit code: $LASTEXITCODE"
exit $LASTEXITCODE
}

View File

@@ -8,7 +8,7 @@ on:
jobs:
update-collab-staging-tag:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

View File

@@ -37,7 +37,7 @@ jobs:
run_nix: ${{ steps.filter.outputs.run_nix }}
run_actionlint: ${{ steps.filter.outputs.run_actionlint }}
runs-on:
- ubuntu-latest
- namespace-profile-2x4-ubuntu-2404
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -81,6 +81,7 @@ jobs:
echo "run_license=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
echo "run_nix=false" >> "$GITHUB_OUTPUT"
@@ -237,7 +238,7 @@ jobs:
uses: ./.github/actions/build_docs
actionlint:
runs-on: ubuntu-latest
runs-on: namespace-profile-2x4-ubuntu-2404
if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true'
needs: [job_spec]
steps:
@@ -418,7 +419,7 @@ jobs:
if: |
github.repository_owner == 'zed-industries' &&
needs.job_spec.outputs.run_tests == 'true'
runs-on: [self-hosted, Windows, X64]
runs-on: [self-32vcpu-windows-2022]
steps:
- name: Environment Setup
run: |
@@ -458,7 +459,7 @@ jobs:
tests_pass:
name: Tests Pass
runs-on: ubuntu-latest
runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- job_spec
- style
@@ -784,7 +785,7 @@ jobs:
bundle-windows-x64:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-hosted, Windows, X64]
runs-on: [self-32vcpu-windows-2022]
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]

View File

@@ -12,7 +12,7 @@ on:
jobs:
danger:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

View File

@@ -59,7 +59,7 @@ jobs:
timeout-minutes: 60
name: Run tests on Windows
if: github.repository_owner == 'zed-industries'
runs-on: [self-hosted, Windows, X64]
runs-on: [self-32vcpu-windows-2022]
steps:
- name: Checkout repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
@@ -206,9 +206,6 @@ jobs:
runs-on: github-8vcpu-ubuntu-2404
needs: tests
name: Build Zed on FreeBSD
# env:
# MYTOKEN : ${{ secrets.MYTOKEN }}
# MYTOKEN2: "value2"
steps:
- uses: actions/checkout@v4
- name: Build FreeBSD remote-server
@@ -243,7 +240,6 @@ jobs:
bundle-nix:
name: Build and cache Nix package
if: false
needs: tests
secrets: inherit
uses: ./.github/workflows/nix.yml
@@ -252,7 +248,7 @@ jobs:
timeout-minutes: 60
name: Create a Windows installer
if: github.repository_owner == 'zed-industries'
runs-on: [self-hosted, Windows, X64]
runs-on: [self-32vcpu-windows-2022]
needs: windows-tests
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
@@ -294,7 +290,7 @@ jobs:
update-nightly-tag:
name: Update nightly tag
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
runs-on: namespace-profile-2x4-ubuntu-2404
needs:
- bundle-mac
- bundle-linux-x86

View File

@@ -12,7 +12,7 @@ jobs:
shellcheck:
name: "ShellCheck Scripts"
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

13
.rules
View File

@@ -12,6 +12,19 @@
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
* Avoid creative additions unless explicitly requested
* Use full words for variable names (no abbreviations like "q" for "queue")
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
Example:
```rust
executor.spawn({
let task_ran = task_ran.clone();
async move {
*task_ran.borrow_mut() = true;
}
});
```
# GPUI

View File

@@ -27,6 +27,22 @@ By effectively engaging with the Zed team and community early in your process, w
We plan to set aside time each week to pair program with contributors on promising pull requests in Zed. This will be an experiment. We tend to prefer pairing over async code review on our team, and we'd like to see how well it works in an open source setting. If we're finding it difficult to get on the same page with async review, we may ask you to pair with us if you're open to it. The closer a contribution is to the goals outlined in our roadmap, the more likely we'll be to spend time pairing on it.
## Mandatory PR contents
Please ensure the PR contains
- Before & after screenshots, if there are visual adjustments introduced.
Examples of visual adjustments: tree-sitter query updates, UI changes, etc.
- A disclosure of the AI assistance usage, if any was used.
Any kind of AI assistance must be disclosed in the PR, along with the extent to which AI assistance was used (e.g. docs only vs. code generation).
If the PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, as long as it's limited to single keywords or short phrases.
## Tips to improve the chances of your PR getting reviewed and merged
- Discuss your plans ahead of time with the team
@@ -49,6 +65,8 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
## Bird's-eye view of Zed
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
- [`gpui`](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation.**

334
Cargo.lock generated
View File

@@ -23,13 +23,15 @@ dependencies = [
"language_model",
"markdown",
"parking_lot",
"portable-pty",
"project",
"prompt_store",
"rand 0.8.5",
"rand 0.9.1",
"serde",
"serde_json",
"settings",
"smol",
"task",
"tempfile",
"terminal",
"ui",
@@ -37,6 +39,7 @@ dependencies = [
"util",
"uuid",
"watch",
"which 6.0.3",
"workspace-hack",
]
@@ -76,7 +79,7 @@ dependencies = [
"log",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"serde_json",
"settings",
"text",
@@ -169,7 +172,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"rand 0.9.1",
"ref-cast",
"rope",
"schemars",
@@ -187,14 +190,15 @@ dependencies = [
"uuid",
"workspace",
"workspace-hack",
"zed_env_vars",
"zstd",
]
[[package]]
name = "agent-client-protocol"
version = "0.1.1"
version = "0.2.0-alpha.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b91e5ec3ce05e8effb2a7a3b7b1a587daa6699b9f98bbde6a35e44b8c6c773a"
checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
dependencies = [
"anyhow",
"async-broadcast",
@@ -248,7 +252,6 @@ dependencies = [
"open",
"parking_lot",
"paths",
"portable-pty",
"pretty_assertions",
"project",
"prompt_store",
@@ -274,9 +277,9 @@ dependencies = [
"uuid",
"watch",
"web_search",
"which 6.0.3",
"workspace-hack",
"worktree",
"zed_env_vars",
"zlog",
"zstd",
]
@@ -405,7 +408,7 @@ dependencies = [
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"rand 0.9.1",
"release_channel",
"rope",
"rules_library",
@@ -507,7 +510,7 @@ dependencies = [
"parking_lot",
"piper",
"polling",
"regex-automata 0.4.9",
"regex-automata",
"rustix-openpty",
"serde",
"signal-hook",
@@ -831,7 +834,7 @@ dependencies = [
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rpc",
"serde",
@@ -847,6 +850,7 @@ dependencies = [
"uuid",
"workspace",
"workspace-hack",
"zed_env_vars",
]
[[package]]
@@ -929,7 +933,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"serde",
"serde_json",
@@ -981,7 +985,7 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"reqwest_client",
"rust-embed",
@@ -1382,10 +1386,11 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"derive_more",
"gpui",
"parking_lot",
"rodio",
"schemars",
"serde",
"settings",
"util",
"workspace-hack",
]
@@ -2286,7 +2291,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"ash",
"ash-window",
@@ -2319,7 +2324,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"proc-macro2",
"quote",
@@ -2329,7 +2334,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2456,7 +2461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"serde",
]
@@ -2473,7 +2478,7 @@ dependencies = [
"language",
"log",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"rope",
"serde_json",
"sum_tree",
@@ -2894,7 +2899,7 @@ dependencies = [
"language",
"log",
"postage",
"rand 0.8.5",
"rand 0.9.1",
"release_channel",
"rpc",
"settings",
@@ -3081,7 +3086,7 @@ dependencies = [
"parking_lot",
"paths",
"postage",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"rpc",
@@ -3330,7 +3335,7 @@ dependencies = [
"prometheus",
"prompt_store",
"prost 0.9.0",
"rand 0.8.5",
"rand 0.9.1",
"recent_projects",
"release_channel",
"remote",
@@ -4051,6 +4056,7 @@ dependencies = [
name = "crashes"
version = "0.1.0"
dependencies = [
"bincode",
"crash-handler",
"log",
"mach2 0.5.0",
@@ -4060,6 +4066,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
"system_specs",
"workspace-hack",
]
@@ -4485,6 +4492,7 @@ dependencies = [
"tempfile",
"util",
"workspace-hack",
"zed_env_vars",
]
[[package]]
@@ -4681,7 +4689,6 @@ dependencies = [
"component",
"ctor",
"editor",
"futures 0.3.31",
"gpui",
"indoc",
"language",
@@ -4690,7 +4697,7 @@ dependencies = [
"markdown",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"serde",
"serde_json",
"settings",
@@ -4730,7 +4737,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291"
dependencies = [
"nu-ansi-term 0.50.1",
"nu-ansi-term",
]
[[package]]
@@ -5061,7 +5068,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"rpc",
@@ -5556,7 +5563,7 @@ dependencies = [
"parking_lot",
"paths",
"project",
"rand 0.8.5",
"rand 0.9.1",
"release_channel",
"remote",
"reqwest_client",
@@ -5629,8 +5636,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set 0.5.3",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -5640,8 +5647,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -5718,14 +5725,10 @@ dependencies = [
name = "feedback"
version = "0.1.0"
dependencies = [
"client",
"editor",
"gpui",
"human_bytes",
"menu",
"release_channel",
"serde",
"sysinfo",
"system_specs",
"ui",
"urlencoding",
"util",
@@ -6409,7 +6412,7 @@ dependencies = [
"log",
"parking_lot",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rope",
"schemars",
@@ -7295,8 +7298,8 @@ dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -7462,7 +7465,7 @@ dependencies = [
"pathfinder_geometry",
"postage",
"profiling",
"rand 0.8.5",
"rand 0.9.1",
"raw-window-handle",
"refineable",
"reqwest_client",
@@ -8301,7 +8304,7 @@ dependencies = [
"globset",
"log",
"memchr",
"regex-automata 0.4.9",
"regex-automata",
"same-file",
"walkdir",
"winapi-util",
@@ -8468,6 +8471,7 @@ dependencies = [
"theme",
"ui",
"util",
"util_macros",
"workspace",
"workspace-hack",
"zed_actions",
@@ -8899,7 +8903,7 @@ dependencies = [
"percent-encoding",
"referencing",
"regex",
"regex-syntax 0.8.5",
"regex-syntax",
"reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
"serde",
"serde_json",
@@ -8952,6 +8956,44 @@ dependencies = [
"uuid",
]
[[package]]
name = "keymap_editor"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"component",
"db",
"editor",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -9036,7 +9078,7 @@ dependencies = [
"parking_lot",
"postage",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rpc",
"schemars",
@@ -9109,6 +9151,7 @@ dependencies = [
"icons",
"image",
"log",
"open_router",
"parking_lot",
"proto",
"schemars",
@@ -9212,6 +9255,7 @@ dependencies = [
"language",
"lsp",
"project",
"proto",
"release_channel",
"serde_json",
"settings",
@@ -9604,6 +9648,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"audio",
"collections",
"core-foundation 0.10.0",
"core-video",
@@ -9626,6 +9671,7 @@ dependencies = [
"scap",
"serde",
"serde_json",
"settings",
"sha2",
"simplelog",
"smallvec",
@@ -9698,7 +9744,7 @@ dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax 0.8.5",
"regex-syntax",
"rustc_version",
"syn 2.0.101",
]
@@ -9770,7 +9816,7 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.95.1"
source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec"
source = "git+https://github.com/zed-industries/lsp-types?rev=0874f8742fe55b4dc94308c1e3c0069710d8eeaf#0874f8742fe55b4dc94308c1e3c0069710d8eeaf"
dependencies = [
"bitflags 1.3.2",
"serde",
@@ -9913,9 +9959,11 @@ dependencies = [
"editor",
"fs",
"gpui",
"html5ever 0.27.0",
"language",
"linkify",
"log",
"markup5ever_rcdom",
"pretty_assertions",
"pulldown-cmark 0.12.2",
"settings",
@@ -9976,11 +10024,11 @@ dependencies = [
[[package]]
name = "matchers"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
@@ -10344,7 +10392,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"project",
"rand 0.8.5",
"rand 0.9.1",
"rope",
"serde",
"settings",
@@ -10681,16 +10729,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.1"
@@ -11189,6 +11227,8 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"strum 0.27.1",
"thiserror 2.0.12",
"workspace-hack",
]
@@ -11384,12 +11424,6 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.11.1"
@@ -11612,6 +11646,12 @@ dependencies = [
"hmac",
]
[[package]]
name = "pciid-parser"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
[[package]]
name = "pem"
version = "3.0.5"
@@ -12578,7 +12618,7 @@ dependencies = [
"postage",
"prettier",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"remote",
@@ -13374,17 +13414,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
"regex-automata",
"regex-syntax",
]
[[package]]
@@ -13395,7 +13426,7 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
"regex-syntax",
]
[[package]]
@@ -13404,12 +13435,6 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a"
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
@@ -13512,6 +13537,7 @@ dependencies = [
"smol",
"sysinfo",
"telemetry_events",
"thiserror 2.0.12",
"toml 0.8.20",
"unindent",
"util",
@@ -13726,7 +13752,6 @@ dependencies = [
"regex",
"reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
"serde",
"smol",
"tokio",
"workspace-hack",
]
@@ -13867,7 +13892,7 @@ dependencies = [
"ctor",
"gpui",
"log",
"rand 0.8.5",
"rand 0.9.1",
"rayon",
"smallvec",
"sum_tree",
@@ -13896,7 +13921,7 @@ dependencies = [
"gpui",
"parking_lot",
"proto",
"rand 0.8.5",
"rand 0.9.1",
"rsa",
"serde",
"serde_json",
@@ -14331,6 +14356,19 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scheduler"
version = "0.1.0"
dependencies = [
"async-task",
"chrono",
"futures 0.3.31",
"parking",
"parking_lot",
"rand 0.9.1",
"workspace-hack",
]
[[package]]
name = "schema_generator"
version = "0.1.0"
@@ -14847,6 +14885,8 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_json_lenient",
"serde_path_to_error",
"settings_ui_macros",
"smallvec",
"tree-sitter",
"tree-sitter-json",
@@ -14882,39 +14922,30 @@ name = "settings_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"command_palette",
"command_palette_hooks",
"component",
"db",
"debugger_ui",
"editor",
"feature_flags",
"fs",
"fuzzy",
"gpui",
"itertools 0.14.0",
"language",
"log",
"menu",
"notifications",
"paths",
"project",
"search",
"serde",
"serde_json",
"settings",
"telemetry",
"tempfile",
"smallvec",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
"ui",
"ui_input",
"util",
"vim",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "settings_ui_macros"
version = "0.1.0"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.101",
"workspace-hack",
]
[[package]]
@@ -15637,7 +15668,7 @@ name = "streaming_diff"
version = "0.1.0"
dependencies = [
"ordered-float 2.10.1",
"rand 0.8.5",
"rand 0.9.1",
"rope",
"util",
"workspace-hack",
@@ -15751,7 +15782,7 @@ dependencies = [
"arrayvec",
"ctor",
"log",
"rand 0.8.5",
"rand 0.9.1",
"rayon",
"workspace-hack",
"zlog",
@@ -16130,6 +16161,21 @@ dependencies = [
"winx",
]
[[package]]
name = "system_specs"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"gpui",
"human_bytes",
"pciid-parser",
"release_channel",
"serde",
"sysinfo",
"workspace-hack",
]
[[package]]
name = "tab_switcher"
version = "0.1.0"
@@ -16327,7 +16373,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"libc",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"schemars",
@@ -16375,7 +16421,7 @@ dependencies = [
"language",
"log",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"schemars",
"search",
@@ -16407,7 +16453,7 @@ dependencies = [
"log",
"parking_lot",
"postage",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rope",
"smallvec",
@@ -16715,6 +16761,7 @@ dependencies = [
"db",
"gpui",
"http_client",
"keymap_editor",
"notifications",
"pretty_assertions",
"project",
@@ -16723,7 +16770,6 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@@ -17092,14 +17138,14 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
dependencies = [
"matchers",
"nu-ansi-term 0.46.0",
"nu-ansi-term",
"once_cell",
"regex",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
@@ -17130,7 +17176,7 @@ checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0"
dependencies = [
"cc",
"regex",
"regex-syntax 0.8.5",
"regex-syntax",
"serde_json",
"streaming-iterator",
"tree-sitter-language",
@@ -17160,8 +17206,7 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609"
dependencies = [
"cc",
"tree-sitter-language",
@@ -17765,7 +17810,7 @@ dependencies = [
"libc",
"log",
"nix 0.29.0",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"rust-embed",
"schemars",
@@ -17950,6 +17995,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"schemars",
"serde",
"settings",
"workspace-hack",
]
@@ -18556,7 +18603,7 @@ dependencies = [
"futures 0.3.31",
"gpui",
"parking_lot",
"rand 0.8.5",
"rand 0.9.1",
"workspace-hack",
"zlog",
]
@@ -19764,7 +19811,6 @@ dependencies = [
"any_vec",
"anyhow",
"async-recursion",
"bincode",
"call",
"client",
"clock",
@@ -19783,6 +19829,7 @@ dependencies = [
"node_runtime",
"parking_lot",
"postage",
"pretty_assertions",
"project",
"remote",
"schemars",
@@ -19929,8 +19976,8 @@ dependencies = [
"rand_core 0.6.4",
"regalloc2",
"regex",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
"regex-automata",
"regex-syntax",
"ring",
"rust_decimal",
"rustc-hash 1.1.0",
@@ -20015,7 +20062,7 @@ dependencies = [
"paths",
"postage",
"pretty_assertions",
"rand 0.8.5",
"rand 0.9.1",
"rpc",
"schemars",
"serde",
@@ -20112,9 +20159,9 @@ dependencies = [
[[package]]
name = "xcb"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
checksum = "f07c123b796139bfe0603e654eaf08e132e52387ba95b252c78bad3640ba37ea"
dependencies = [
"bitflags 1.3.2",
"libc",
@@ -20371,7 +20418,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.201.8"
version = "0.204.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -20389,6 +20436,7 @@ dependencies = [
"auto_update",
"auto_update_ui",
"backtrace",
"bincode",
"breadcrumbs",
"call",
"channel",
@@ -20434,6 +20482,7 @@ dependencies = [
"itertools 0.14.0",
"jj_ui",
"journal",
"keymap_editor",
"language",
"language_extension",
"language_model",
@@ -20487,6 +20536,7 @@ dependencies = [
"supermaven",
"svg_preview",
"sysinfo",
"system_specs",
"tab_switcher",
"task",
"tasks_ui",
@@ -20518,6 +20568,7 @@ dependencies = [
"workspace",
"workspace-hack",
"zed_actions",
"zed_env_vars",
"zeta",
"zlog",
"zlog_settings",
@@ -20534,6 +20585,13 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "zed_env_vars"
version = "0.1.0"
dependencies = [
"workspace-hack",
]
[[package]]
name = "zed_extension_api"
version = "0.1.0"
@@ -20563,7 +20621,7 @@ dependencies = [
[[package]]
name = "zed_html"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -20597,13 +20655,6 @@ dependencies = [
"zed_extension_api 0.6.0",
]
[[package]]
name = "zed_toml"
version = "0.1.4"
dependencies = [
"zed_extension_api 0.1.0",
]
[[package]]
name = "zeno"
version = "0.3.2"
@@ -20762,13 +20813,14 @@ dependencies = [
"gpui",
"http_client",
"indoc",
"itertools 0.14.0",
"language",
"language_model",
"log",
"menu",
"postage",
"project",
"rand 0.8.5",
"rand 0.9.1",
"regex",
"release_channel",
"reqwest_client",
@@ -20776,6 +20828,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"strum 0.27.1",
"telemetry",
"telemetry_events",
"theme",
@@ -20783,7 +20836,6 @@ dependencies = [
"tree-sitter-go",
"tree-sitter-rust",
"ui",
"unindent",
"util",
"uuid",
"workspace",

View File

@@ -54,6 +54,8 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -82,13 +84,12 @@ members = [
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
@@ -130,6 +131,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/scheduler",
"crates/remote",
"crates/remote_server",
"crates/repl",
@@ -146,6 +148,7 @@ members = [
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -158,6 +161,7 @@ members = [
"crates/supermaven",
"crates/supermaven_api",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -190,6 +194,7 @@ members = [
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
"crates/zed_env_vars",
"crates/zeta",
"crates/zeta_cli",
"crates/zlog",
@@ -206,7 +211,6 @@ members = [
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/test-extension",
"extensions/toml",
#
# Tooling
@@ -296,9 +300,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
] }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
@@ -313,6 +315,7 @@ install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
@@ -357,6 +360,7 @@ proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
@@ -372,6 +376,7 @@ semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_ui = { path = "crates/settings_ui" }
settings_ui_macros = { path = "crates/settings_ui_macros" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
@@ -383,6 +388,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
@@ -416,6 +422,7 @@ worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" }
zeta = { path = "crates/zeta" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
@@ -424,7 +431,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = "0.1"
agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]}
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -438,6 +445,7 @@ async-fs = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
@@ -451,10 +459,11 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -494,6 +503,7 @@ handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
@@ -515,7 +525,7 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
@@ -530,9 +540,11 @@ objc = "0.2"
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking = "2.0"
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -551,7 +563,7 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.8.5"
rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
@@ -583,6 +595,7 @@ serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
@@ -619,7 +632,7 @@ tower-http = "0.4.4"
tree-sitter = { version = "0.25.6", features = ["wasm"] }
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"
tree-sitter-diff = "0.1.0"
tree-sitter-elixir = "0.3"
@@ -686,6 +699,7 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
@@ -803,6 +817,12 @@ unexpected_cfgs = { level = "allow" }
dbg_macro = "deny"
todo = "deny"
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
# Remove when the lint gets promoted to `suspicious`.
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.
#
@@ -831,6 +851,9 @@ too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
nonminimal_bool = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",

2
Procfile.web Normal file
View File

@@ -0,0 +1,2 @@
postgrest_llm: postgrest crates/collab/postgrest_llm.conf
website: cd ../zed.dev; npm run dev -- --port=3000

View File

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

After

Width:  |  Height:  |  Size: 305 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -16,7 +16,6 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -41,7 +40,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
@@ -64,8 +63,8 @@
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
@@ -121,7 +120,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"ctrl-alt-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -131,8 +130,8 @@
"bindings": {
"shift-enter": "editor::Newline",
"enter": "editor::Newline",
"ctrl-enter": "editor::NewlineAbove",
"ctrl-shift-enter": "editor::NewlineBelow",
"ctrl-enter": "editor::NewlineBelow",
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
@@ -171,6 +170,7 @@
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
"ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -259,6 +259,7 @@
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -856,7 +857,7 @@
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1195,9 +1196,16 @@
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage",
"ctrl-escape": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
}
]

View File

@@ -70,9 +70,9 @@
"cmd-k q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cmd-x": "editor::Cut",
"cmd-c": "editor::Copy",
"cmd-v": "editor::Paste",
@@ -915,7 +915,7 @@
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"ctrl-shift-enter": "workspace::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
@@ -1301,5 +1301,12 @@
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "workspace::OpenWithSystem"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -38,10 +38,11 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save

View File

@@ -50,8 +50,8 @@
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-right": "editor::MoveToNextSubwordEnd",
"alt-left": "editor::MoveToPreviousSubwordStart",
"alt-shift-right": "editor::SelectToNextSubwordEnd",

View File

@@ -38,10 +38,11 @@
"alt-;": ["editor::ToggleComments", { "advance_downwards": false }],
"ctrl-x ctrl-;": "editor::ToggleComments",
"alt-.": "editor::GoToDefinition", // xref-find-definitions
"alt-?": "editor::FindAllReferences", // xref-find-references
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save

View File

@@ -52,8 +52,8 @@
"cmd-k cmd-l": "editor::ConvertToLowerCase",
"cmd-shift-j": "editor::JoinLines",
"shift-alt-m": "markdown::OpenPreviewToTheSide",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-right": "editor::MoveToNextSubwordEnd",
"ctrl-left": "editor::MoveToPreviousSubwordStart",
"ctrl-shift-right": "editor::SelectToNextSubwordEnd",

View File

@@ -21,10 +21,10 @@
{
"context": "Editor",
"bindings": {
"alt-backspace": "editor::DeleteToPreviousWordStart",
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-shift-delete": "editor::DeleteToNextWordEnd",
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-shift-backspace": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-shift-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
"alt-left": ["editor::MoveToPreviousWordStart", { "stop_at_soft_wraps": true }],

View File

@@ -337,7 +337,7 @@
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
@@ -354,6 +354,15 @@
"ctrl-s": "editor::ShowSignatureHelp"
}
},
{
"context": "showing_completions",
"bindings": {
"ctrl-d": "vim::ScrollDown",
"ctrl-u": "vim::ScrollUp",
"ctrl-e": "vim::LineDown",
"ctrl-y": "vim::LineUp"
}
},
{
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
@@ -428,12 +437,14 @@
"g h": "vim::StartOfLine",
"g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s"
"g e": "vim::EndOfDocument",
"g .": "vim::HelixGotoLastModification", // go to last modification
"g r": "editor::FindAllReferences", // zed specific
"g t": "vim::WindowTop",
"g c": "vim::WindowMiddle",
"g b": "vim::WindowBottom",
"x": "editor::SelectLine",
"shift-r": "editor::Paste",
"x": "vim::HelixSelectLine",
"shift-x": "editor::SelectLine",
"%": "editor::SelectAll",
// Window mode
@@ -819,7 +830,7 @@
"v": "project_panel::OpenPermanent",
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
"s": "project_panel::OpenWithSystem",
"s": "workspace::OpenWithSystem",
"z d": "project_panel::CompareMarkedFiles",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",

View File

@@ -172,7 +172,7 @@ The user has specified the following rules that should be applied:
Rules title: {{title}}
{{/if}}
``````
{{contents}}}
{{contents}}
``````
{{/each}}
{{/if}}

View File

@@ -162,6 +162,12 @@
// 2. Always quit the application
// "on_last_window_closed": "quit_app",
"on_last_window_closed": "platform_default",
// Whether to show padding for zoomed panels.
// When enabled, zoomed center panels (e.g. code editor) will have padding all around,
// while zoomed bottom/left/right panels will have padding to the top/right/left (respectively).
//
// Default: true
"zoomed_padding": true,
// Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true,
@@ -182,8 +188,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Default: "bar"
"cursor_shape": "bar",
// Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
@@ -217,9 +223,25 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// Whether the text selection should have rounded corners.
"rounded_selection": true,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// This only affects text drawn over highlight backgrounds in the editor.
"minimum_contrast_for_highlights": 45,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -260,8 +282,8 @@
// - "warning"
// - "info"
// - "hint"
// - null — allow all diagnostics (default)
"diagnostics_max_severity": null,
// - "all" — allow all diagnostics (default)
"diagnostics_max_severity": "all",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -273,6 +295,8 @@
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
// The default number of context lines shown in multibuffer excerpts.
"excerpt_context_lines": 2,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
@@ -357,6 +381,8 @@
// Whether to show code action buttons in the editor toolbar.
"code_actions": false
},
// Whether to allow windows to tab together based on the users tabbing preference (macOS only).
"use_system_window_tabs": false,
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
@@ -647,6 +673,8 @@
// "never"
"show": "always"
},
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
"hide_root": false
},
@@ -1133,11 +1161,6 @@
// The minimum severity of the diagnostics to show inline.
// Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": null
},
"cargo": {
// When enabled, Zed disables rust-analyzer's check on save and starts to query
// Cargo diagnostics separately.
"fetch_cargo_diagnostics": false
}
},
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@@ -1503,6 +1526,11 @@
//
// Default: fallback
"words": "fallback",
// Minimum number of characters required to automatically trigger word-based completions.
// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command.
//
// Default: 3
"words_min_length": 3,
// Whether to fetch LSP completions or not.
//
// Default: true
@@ -1629,6 +1657,9 @@
"allowed": true
}
},
"Kotlin": {
"language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."]
},
"LaTeX": {
"formatter": "language_server",
"language_servers": ["texlab", "..."],
@@ -1642,9 +1673,6 @@
"use_on_type_format": false,
"allow_rewrap": "anywhere",
"soft_wrap": "editor_width",
"completions": {
"words": "disabled"
},
"prettier": {
"allowed": true
}
@@ -1658,9 +1686,6 @@
}
},
"Plain Text": {
"completions": {
"words": "disabled"
},
"allow_rewrap": "anywhere"
},
"Python": {
@@ -1751,7 +1776,7 @@
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
"api_url": "https://api.deepseek.com"
"api_url": "https://api.deepseek.com/v1"
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
@@ -1899,7 +1924,10 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
"timeout": 2000,
"dock": "bottom",
"log_dap_communications": true,
"format_dap_log_messages": true,
"button": true
},
// Configures any number of settings profiles that are temporarily applied on

View File

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

View File

@@ -31,18 +31,21 @@ language.workspace = true
language_model.workspace = true
markdown.workspace = true
parking_lot = { workspace = true, optional = true }
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
task.workspace = true
terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View File

@@ -7,6 +7,7 @@ use agent_settings::AgentSettings;
use collections::HashSet;
pub use connection::*;
pub use diff::*;
use futures::future::Shared;
use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
@@ -15,7 +16,7 @@ use settings::Settings as _;
pub use terminal::*;
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result, anyhow};
use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
@@ -33,7 +34,8 @@ use std::rc::Rc;
use std::time::{Duration, Instant};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
use util::ResultExt;
use util::{ResultExt, get_system_shell};
use uuid::Uuid;
#[derive(Debug)]
pub struct UserMessage {
@@ -183,37 +185,46 @@ impl ToolCall {
tool_call: acp::ToolCall,
status: ToolCallStatus,
language_registry: Arc<LanguageRegistry>,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) -> Self {
) -> Result<Self> {
let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") {
first_line.to_owned() + ""
} else {
tool_call.title
};
Self {
let mut content = Vec::with_capacity(tool_call.content.len());
for item in tool_call.content {
content.push(ToolCallContent::from_acp(
item,
language_registry.clone(),
terminals,
cx,
)?);
}
let result = Self {
id: tool_call.id,
label: cx
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
kind: tool_call.kind,
content: tool_call
.content
.into_iter()
.map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx))
.collect(),
content,
locations: tool_call.locations,
resolved_locations: Vec::default(),
status,
raw_input: tool_call.raw_input,
raw_output: tool_call.raw_output,
}
};
Ok(result)
}
fn update_fields(
&mut self,
fields: acp::ToolCallUpdateFields,
language_registry: Arc<LanguageRegistry>,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) {
) -> Result<()> {
let acp::ToolCallUpdateFields {
kind,
status,
@@ -248,14 +259,15 @@ impl ToolCall {
// Reuse existing content if we can
for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
old.update_from_acp(new, language_registry.clone(), cx);
old.update_from_acp(new, language_registry.clone(), terminals, cx)?;
}
for new in content {
self.content.push(ToolCallContent::from_acp(
new,
language_registry.clone(),
terminals,
cx,
))
)?)
}
self.content.truncate(new_content_len);
}
@@ -279,6 +291,7 @@ impl ToolCall {
}
self.raw_output = Some(raw_output);
}
Ok(())
}
pub fn diffs(&self) -> impl Iterator<Item = &Entity<Diff>> {
@@ -549,13 +562,16 @@ impl ToolCallContent {
pub fn from_acp(
content: acp::ToolCallContent,
language_registry: Arc<LanguageRegistry>,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) -> Self {
) -> Result<Self> {
match content {
acp::ToolCallContent::Content { content } => {
Self::ContentBlock(ContentBlock::new(content, &language_registry, cx))
}
acp::ToolCallContent::Diff { diff } => Self::Diff(cx.new(|cx| {
acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
content,
&language_registry,
cx,
))),
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
Diff::finalized(
diff.path,
diff.old_text,
@@ -563,7 +579,12 @@ impl ToolCallContent {
language_registry,
cx,
)
})),
}))),
acp::ToolCallContent::Terminal { terminal_id } => terminals
.get(&terminal_id)
.cloned()
.map(Self::Terminal)
.ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
}
}
@@ -571,8 +592,9 @@ impl ToolCallContent {
&mut self,
new: acp::ToolCallContent,
language_registry: Arc<LanguageRegistry>,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) {
) -> Result<()> {
let needs_update = match (&self, &new) {
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
old_diff.read(cx).needs_update(
@@ -585,8 +607,9 @@ impl ToolCallContent {
};
if needs_update {
*self = Self::from_acp(new, language_registry, cx);
*self = Self::from_acp(new, language_registry, terminals, cx)?;
}
Ok(())
}
pub fn to_markdown(&self, cx: &App) -> String {
@@ -763,6 +786,8 @@ pub struct AcpThread {
token_usage: Option<TokenUsage>,
prompt_capabilities: acp::PromptCapabilities,
_observe_prompt_capabilities: Task<anyhow::Result<()>>,
determine_shell: Shared<Task<String>>,
terminals: HashMap<acp::TerminalId, Entity<Terminal>>,
}
#[derive(Debug)]
@@ -778,6 +803,8 @@ pub enum AcpThreadEvent {
Error,
LoadError(LoadError),
PromptCapabilitiesUpdated,
Refusal,
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
@@ -846,6 +873,20 @@ impl AcpThread {
}
});
let determine_shell = cx
.background_spawn(async move {
if cfg!(windows) {
return get_system_shell();
}
if which::which("bash").is_ok() {
"bash".into()
} else {
get_system_shell()
}
})
.shared();
Self {
action_log,
shared_buffers: Default::default(),
@@ -859,6 +900,8 @@ impl AcpThread {
token_usage: None,
prompt_capabilities,
_observe_prompt_capabilities: task,
terminals: HashMap::default(),
determine_shell,
}
}
@@ -961,6 +1004,9 @@ impl AcpThread {
acp::SessionUpdate::Plan(plan) => {
self.update_plan(plan, cx);
}
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
}
}
Ok(())
}
@@ -1082,27 +1128,28 @@ impl AcpThread {
let update = update.into();
let languages = self.project.read(cx).languages().clone();
let (ix, current_call) = self
.tool_call_mut(update.id())
let ix = self
.index_for_tool_call(update.id())
.context("Tool call not found")?;
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
unreachable!()
};
match update {
ToolCallUpdate::UpdateFields(update) => {
let location_updated = update.fields.locations.is_some();
current_call.update_fields(update.fields, languages, cx);
call.update_fields(update.fields, languages, &self.terminals, cx)?;
if location_updated {
self.resolve_locations(update.id, cx);
}
}
ToolCallUpdate::UpdateDiff(update) => {
current_call.content.clear();
current_call
.content
.push(ToolCallContent::Diff(update.diff));
call.content.clear();
call.content.push(ToolCallContent::Diff(update.diff));
}
ToolCallUpdate::UpdateTerminal(update) => {
current_call.content.clear();
current_call
.content
call.content.clear();
call.content
.push(ToolCallContent::Terminal(update.terminal));
}
}
@@ -1125,21 +1172,30 @@ impl AcpThread {
/// Fails if id does not match an existing entry.
pub fn upsert_tool_call_inner(
&mut self,
tool_call_update: acp::ToolCallUpdate,
update: acp::ToolCallUpdate,
status: ToolCallStatus,
cx: &mut Context<Self>,
) -> Result<(), acp::Error> {
let language_registry = self.project.read(cx).languages().clone();
let id = tool_call_update.id.clone();
let id = update.id.clone();
if let Some((ix, current_call)) = self.tool_call_mut(&id) {
current_call.update_fields(tool_call_update.fields, language_registry, cx);
current_call.status = status;
if let Some(ix) = self.index_for_tool_call(&id) {
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
unreachable!()
};
call.update_fields(update.fields, language_registry, &self.terminals, cx)?;
call.status = status;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
} else {
let call =
ToolCall::from_acp(tool_call_update.try_into()?, status, language_registry, cx);
let call = ToolCall::from_acp(
update.try_into()?,
status,
language_registry,
&self.terminals,
cx,
)?;
self.push_entry(AgentThreadEntry::ToolCall(call), cx);
};
@@ -1147,6 +1203,22 @@ impl AcpThread {
Ok(())
}
fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option<usize> {
self.entries
.iter()
.enumerate()
.rev()
.find_map(|(index, entry)| {
if let AgentThreadEntry::ToolCall(tool_call) = entry
&& &tool_call.id == id
{
Some(index)
} else {
None
}
})
}
fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
// The tool call we are looking for is typically the last one, or very close to the end.
// At the moment, it doesn't seem like a hashmap would be a good fit for this use case.
@@ -1495,15 +1567,42 @@ impl AcpThread {
this.send_task.take();
}
// Truncate entries if the last prompt was refused.
// Handle refusal - distinguish between user prompt and tool call refusals
if let Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
})) = result
&& let Some((ix, _)) = this.last_user_message()
{
let range = ix..this.entries.len();
this.entries.truncate(ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range));
if let Some((user_msg_ix, _)) = this.last_user_message() {
// Check if there's a completed tool call with results after the last user message
// This indicates the refusal is in response to tool output, not the user's prompt
let has_completed_tool_call_after_user_msg =
this.entries.iter().skip(user_msg_ix + 1).any(|entry| {
if let AgentThreadEntry::ToolCall(tool_call) = entry {
// Check if the tool call has completed and has output
matches!(tool_call.status, ToolCallStatus::Completed)
&& tool_call.raw_output.is_some()
} else {
false
}
});
if has_completed_tool_call_after_user_msg {
// Refusal is due to tool output - don't truncate, just notify
// The model refused based on what the tool returned
cx.emit(AcpThreadEvent::Refusal);
} else {
// User prompt was refused - truncate back to before the user message
let range = user_msg_ix..this.entries.len();
if range.start < range.end {
this.entries.truncate(user_msg_ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range));
}
cx.emit(AcpThreadEvent::Refusal);
}
} else {
// No user message found, treat as general refusal
cx.emit(AcpThreadEvent::Refusal);
}
}
cx.emit(AcpThreadEvent::Stopped);
@@ -1829,6 +1928,133 @@ impl AcpThread {
})
}
pub fn create_terminal(
&self,
mut command: String,
args: Vec<String>,
extra_env: Vec<acp::EnvVariable>,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Terminal>>> {
for arg in args {
command.push(' ');
command.push_str(&arg);
}
let shell_command = if cfg!(windows) {
format!("$null | & {{{}}}", command.replace("\"", "'"))
} else if let Some(cwd) = cwd.as_ref().and_then(|cwd| cwd.as_os_str().to_str()) {
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", command)
} else {
format!("({}) </dev/null", command)
};
let args = vec!["-c".into(), shell_command];
let env = match &cwd {
Some(dir) => self.project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
let env = cx.spawn(async move |_, _| {
let mut env = env.await.unwrap_or_default();
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}
for var in extra_env {
env.insert(var.name, var.value);
}
env
});
let project = self.project.clone();
let language_registry = project.read(cx).languages().clone();
let determine_shell = self.determine_shell.clone();
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
let program = determine_shell.await;
let env = env.await;
let terminal = project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(program),
args,
cwd: cwd.clone(),
env,
..Default::default()
},
cx,
)
})?
.await?;
cx.new(|cx| {
Terminal::new(
terminal_id,
command,
cwd,
output_byte_limit.map(|l| l as usize),
terminal,
language_registry,
cx,
)
})
}
});
cx.spawn(async move |this, cx| {
let terminal = terminal_task.await?;
this.update(cx, |this, _cx| {
this.terminals.insert(terminal_id, terminal.clone());
terminal
})
})
}
pub fn kill_terminal(
&mut self,
terminal_id: acp::TerminalId,
cx: &mut Context<Self>,
) -> Result<()> {
self.terminals
.get(&terminal_id)
.context("Terminal not found")?
.update(cx, |terminal, cx| {
terminal.kill(cx);
});
Ok(())
}
pub fn release_terminal(
&mut self,
terminal_id: acp::TerminalId,
cx: &mut Context<Self>,
) -> Result<()> {
self.terminals
.remove(&terminal_id)
.context("Terminal not found")?
.update(cx, |terminal, cx| {
terminal.kill(cx);
});
Ok(())
}
pub fn terminal(&self, terminal_id: acp::TerminalId) -> Result<Entity<Terminal>> {
self.terminals
.get(&terminal_id)
.context("Terminal not found")
.cloned()
}
pub fn to_markdown(&self, cx: &App) -> String {
self.entries.iter().map(|e| e.to_markdown(cx)).collect()
}
@@ -1888,7 +2114,7 @@ mod tests {
use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
use indoc::indoc;
use project::{FakeFs, Fs};
use rand::Rng as _;
use rand::{distr, prelude::*};
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt as _;
@@ -2480,6 +2706,187 @@ mod tests {
assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]);
}
#[gpui::test]
async fn test_tool_result_refusal(cx: &mut TestAppContext) {
use std::sync::atomic::AtomicUsize;
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, None, cx).await;
// Create a connection that simulates refusal after tool result
let prompt_count = Arc::new(AtomicUsize::new(0));
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
let prompt_count = prompt_count.clone();
move |_request, thread, mut cx| {
let count = prompt_count.fetch_add(1, SeqCst);
async move {
if count == 0 {
// First prompt: Generate a tool call with result
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool1".into()),
title: "Test Tool".into(),
kind: acp::ToolKind::Fetch,
status: acp::ToolCallStatus::Completed,
content: vec![],
locations: vec![],
raw_input: Some(serde_json::json!({"query": "test"})),
raw_output: Some(
serde_json::json!({"result": "inappropriate content"}),
),
}),
cx,
)
.unwrap();
})?;
// Now return refusal because of the tool result
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
})
} else {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
}
}
.boxed_local()
}
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new("/test"), cx))
.await
.unwrap();
// Track if we see a Refusal event
let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
let saw_refusal_event_captured = saw_refusal_event.clone();
thread.update(cx, |_thread, cx| {
cx.subscribe(
&thread,
move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
if matches!(event, AcpThreadEvent::Refusal) {
*saw_refusal_event_captured.lock().unwrap() = true;
}
},
)
.detach();
});
// Send a user message - this will trigger tool call and then refusal
let send_task = thread.update(cx, |thread, cx| {
thread.send(
vec![acp::ContentBlock::Text(acp::TextContent {
text: "Hello".into(),
annotations: None,
})],
cx,
)
});
cx.background_executor.spawn(send_task).detach();
cx.run_until_parked();
// Verify that:
// 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt)
// 2. The user message was NOT truncated
assert!(
*saw_refusal_event.lock().unwrap(),
"Refusal event should be emitted for tool result refusals"
);
thread.read_with(cx, |thread, _| {
let entries = thread.entries();
assert!(entries.len() >= 2, "Should have user message and tool call");
// Verify user message is still there
assert!(
matches!(entries[0], AgentThreadEntry::UserMessage(_)),
"User message should not be truncated"
);
// Verify tool call is there with result
if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] {
assert!(
tool_call.raw_output.is_some(),
"Tool call should have output"
);
} else {
panic!("Expected tool call at index 1");
}
});
}
#[gpui::test]
async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, None, cx).await;
let refuse_next = Arc::new(AtomicBool::new(false));
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
let refuse_next = refuse_next.clone();
move |_request, _thread, _cx| {
if refuse_next.load(SeqCst) {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
})
}
.boxed_local()
} else {
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
})
}
.boxed_local()
}
}
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
// Track if we see a Refusal event
let saw_refusal_event = Arc::new(std::sync::Mutex::new(false));
let saw_refusal_event_captured = saw_refusal_event.clone();
thread.update(cx, |_thread, cx| {
cx.subscribe(
&thread,
move |_thread, _event_thread, event: &AcpThreadEvent, _cx| {
if matches!(event, AcpThreadEvent::Refusal) {
*saw_refusal_event_captured.lock().unwrap() = true;
}
},
)
.detach();
});
// Send a message that will be refused
refuse_next.store(true, SeqCst);
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx)))
.await
.unwrap();
// Verify that a Refusal event WAS emitted for user prompt refusal
assert!(
*saw_refusal_event.lock().unwrap(),
"Refusal event should be emitted for user prompt refusals"
);
// Verify the message was truncated (user prompt refusal)
thread.read_with(cx, |thread, cx| {
assert_eq!(thread.to_markdown(cx), "");
});
}
#[gpui::test]
async fn test_refusal(cx: &mut TestAppContext) {
init_test(cx);
@@ -2543,8 +2950,8 @@ mod tests {
);
});
// Simulate refusing the second message, ensuring the conversation gets
// truncated to before sending it.
// Simulate refusing the second message. The message should be truncated
// when a user prompt is refused.
refuse_next.store(true, SeqCst);
cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx)))
.await
@@ -2650,8 +3057,8 @@ mod tests {
cx: &mut App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId(
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
rand::rng()
.sample_iter(&distr::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>()

View File

@@ -75,7 +75,6 @@ pub trait AgentConnection {
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -232,13 +231,6 @@ impl AgentModelList {
AgentModelList::Grouped(groups) => groups.is_empty(),
}
}
pub fn len(&self) -> usize {
match self {
AgentModelList::Flat(models) => models.len(),
AgentModelList::Grouped(groups) => groups.values().len(),
}
}
}
#[cfg(feature = "test-support")]

View File

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

View File

@@ -1,34 +1,43 @@
use gpui::{App, AppContext, Context, Entity};
use agent_client_protocol as acp;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Context, Entity, Task};
use language::LanguageRegistry;
use markdown::Markdown;
use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant};
pub struct Terminal {
id: acp::TerminalId,
command: Entity<Markdown>,
working_dir: Option<PathBuf>,
terminal: Entity<terminal::Terminal>,
started_at: Instant,
output: Option<TerminalOutput>,
output_byte_limit: Option<usize>,
_output_task: Shared<Task<acp::TerminalExitStatus>>,
}
pub struct TerminalOutput {
pub ended_at: Instant,
pub exit_status: Option<ExitStatus>,
pub was_content_truncated: bool,
pub content: String,
pub original_content_len: usize,
pub content_line_count: usize,
pub finished_with_empty_output: bool,
}
impl Terminal {
pub fn new(
id: acp::TerminalId,
command: String,
working_dir: Option<PathBuf>,
output_byte_limit: Option<usize>,
terminal: Entity<terminal::Terminal>,
language_registry: Arc<LanguageRegistry>,
cx: &mut Context<Self>,
) -> Self {
let command_task = terminal.read(cx).wait_for_completed_task(cx);
Self {
id,
command: cx.new(|cx| {
Markdown::new(
format!("```\n{}\n```", command).into(),
@@ -41,27 +50,93 @@ impl Terminal {
terminal,
started_at: Instant::now(),
output: None,
output_byte_limit,
_output_task: cx
.spawn(async move |this, cx| {
let exit_status = command_task.await;
this.update(cx, |this, cx| {
let (content, original_content_len) = this.truncated_output(cx);
let content_line_count = this.terminal.read(cx).total_lines();
this.output = Some(TerminalOutput {
ended_at: Instant::now(),
exit_status,
content,
original_content_len,
content_line_count,
});
cx.notify();
})
.ok();
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
}
})
.shared(),
}
}
pub fn finish(
&mut self,
exit_status: Option<ExitStatus>,
original_content_len: usize,
truncated_content_len: usize,
content_line_count: usize,
finished_with_empty_output: bool,
cx: &mut Context<Self>,
) {
self.output = Some(TerminalOutput {
ended_at: Instant::now(),
exit_status,
was_content_truncated: truncated_content_len < original_content_len,
original_content_len,
content_line_count,
finished_with_empty_output,
pub fn id(&self) -> &acp::TerminalId {
&self.id
}
pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {
self._output_task.clone()
}
pub fn kill(&mut self, cx: &mut App) {
self.terminal.update(cx, |terminal, _cx| {
terminal.kill_active_task();
});
cx.notify();
}
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
if let Some(output) = self.output.as_ref() {
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalOutputResponse {
output: output.content.clone(),
truncated: output.original_content_len > output.content.len(),
exit_status: Some(acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
}),
}
} else {
let (current_content, original_len) = self.truncated_output(cx);
acp::TerminalOutputResponse {
truncated: current_content.len() < original_len,
output: current_content,
exit_status: None,
}
}
}
fn truncated_output(&self, cx: &App) -> (String, usize) {
let terminal = self.terminal.read(cx);
let mut content = terminal.get_content();
let original_content_len = content.len();
if let Some(limit) = self.output_byte_limit
&& content.len() > limit
{
let mut end_ix = limit.min(content.len());
while !content.is_char_boundary(end_ix) {
end_ix -= 1;
}
// Don't truncate mid-line, clear the remainder of the last line
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
content.truncate(end_ix);
}
(content, original_content_len)
}
pub fn command(&self) -> &Entity<Markdown> {

View File

@@ -2218,7 +2218,7 @@ mod tests {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
for _ in 0..operations {
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..25 => {
action_log.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng);
@@ -2237,7 +2237,7 @@ mod tests {
.unwrap();
}
_ => {
let is_agent_edit = rng.gen_bool(0.5);
let is_agent_edit = rng.random_bool(0.5);
if is_agent_edit {
log::info!("agent edit");
} else {
@@ -2252,7 +2252,7 @@ mod tests {
}
}
if rng.gen_bool(0.2) {
if rng.random_bool(0.2) {
quiesce(&action_log, &buffer, cx);
}
}

View File

@@ -1,11 +1,10 @@
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType};
use editor::Editor;
use extension_host::ExtensionStore;
use extension_host::{ExtensionOperation, ExtensionStore};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter,
InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
Styled, Transformation, Window, actions, percentage,
App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _,
Render, SharedString, StatefulInteractiveElement, Styled, Window, actions,
};
use language::{
BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
@@ -25,7 +24,10 @@ use std::{
sync::Arc,
time::{Duration, Instant},
};
use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{
ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::truncate_and_trailoff;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -82,7 +84,6 @@ impl ActivityIndicator {
) -> Entity<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let workspace_handle = cx.entity();
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
@@ -100,20 +101,6 @@ impl ActivityIndicator {
})
.detach();
cx.subscribe_in(
&workspace_handle,
window,
|activity_indicator, _, event, window, cx| {
if let workspace::Event::ClearActivityIndicator = event
&& activity_indicator.statuses.pop().is_some()
{
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
cx.notify();
}
},
)
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| {
@@ -405,13 +392,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.with_rotate_animation(2)
.into_any_element(),
),
message,
@@ -433,11 +414,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element(),
),
message: format!("Debug: {}", session.read(cx).adapter()),
@@ -460,11 +437,7 @@ impl ActivityIndicator {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element(),
),
message: job_info.message.into(),
@@ -671,8 +644,9 @@ impl ActivityIndicator {
}
// Show any application auto-update info.
if let Some(updater) = &self.auto_updater {
return match &updater.read(cx).status() {
self.auto_updater
.as_ref()
.and_then(|updater| match &updater.read(cx).status() {
AutoUpdateStatus::Checking => Some(Content {
icon: Some(
Icon::new(IconName::Download)
@@ -728,28 +702,49 @@ impl ActivityIndicator {
tooltip_message: None,
}),
AutoUpdateStatus::Idle => None,
};
}
})
.or_else(|| {
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
&& let Some((extension_id, operation)) =
extension_store.outstanding_operations().iter().next()
{
let (message, icon, rotate) = match operation {
ExtensionOperation::Install => (
format!("Installing {extension_id} extension…"),
IconName::LoadCircle,
true,
),
ExtensionOperation::Upgrade => (
format!("Updating {extension_id} extension…"),
IconName::Download,
false,
),
ExtensionOperation::Remove => (
format!("Removing {extension_id} extension…"),
IconName::LoadCircle,
true,
),
};
if let Some(extension_store) =
ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
&& let Some(extension_id) = extension_store.outstanding_operations().keys().next()
{
return Some(Content {
icon: Some(
Icon::new(IconName::Download)
.size(IconSize::Small)
.into_any_element(),
),
message: format!("Updating {extension_id} extension…"),
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&DismissErrorMessage, window, cx)
})),
tooltip_message: None,
});
}
None
Some(Content {
icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| {
if rotate {
this.with_rotate_animation(3).into_any_element()
} else {
this.into_any_element()
}
})),
message,
on_click: Some(Arc::new(|this, window, cx| {
this.dismiss_error_message(&Default::default(), window, cx)
})),
tooltip_message: None,
})
} else {
None
}
})
}
fn version_tooltip_message(version: &VersionCheckType) -> String {

View File

@@ -63,6 +63,7 @@ time.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
zed_env_vars.workspace = true
zstd.workspace = true
[dev-dependencies]

View File

@@ -41,8 +41,7 @@ use std::{
};
use util::ResultExt as _;
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
use zed_env_vars::ZED_STATELESS;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {

View File

@@ -48,7 +48,6 @@ log.workspace = true
open.workspace = true
parking_lot.workspace = true
paths.workspace = true
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
rust-embed.workspace = true
@@ -68,8 +67,8 @@ util.workspace = true
uuid.workspace = true
watch.workspace = true
web_search.workspace = true
which.workspace = true
workspace-hack.workspace = true
zed_env_vars.workspace = true
zstd.workspace = true
[dev-dependencies]

View File

@@ -2,7 +2,7 @@ use crate::{
ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
UserMessageContent, templates::Templates,
};
use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated};
use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated};
use acp_thread::{AcpThread, AgentModelSelector};
use action_log::ActionLog;
use agent_client_protocol as acp;
@@ -10,7 +10,8 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashSet, IndexMap};
use fs::Fs;
use futures::channel::mpsc;
use futures::channel::{mpsc, oneshot};
use futures::future::Shared;
use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
@@ -23,7 +24,7 @@ use prompt_store::{
use settings::update_settings_file;
use std::any::Any;
use std::collections::HashMap;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use util::ResultExt;
@@ -276,13 +277,6 @@ impl NativeAgent {
cx: &mut Context<Self>,
) -> Entity<AcpThread> {
let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
thread.add_default_tools(cx)
});
let thread = thread_handle.read(cx);
let session_id = thread.id().clone();
@@ -301,6 +295,20 @@ impl NativeAgent {
cx,
)
});
let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
thread.add_default_tools(
Rc::new(AcpThreadEnvironment {
acp_thread: acp_thread.downgrade(),
}) as _,
cx,
)
});
let subscriptions = vec![
cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id());
@@ -1001,7 +1009,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
self.0.read_with(cx, |agent, _cx| {
agent.sessions.get(session_id).map(|session| {
Rc::new(NativeAgentSessionEditor {
Rc::new(NativeAgentSessionTruncate {
thread: session.thread.clone(),
acp_thread: session.acp_thread.clone(),
}) as _
@@ -1050,12 +1058,12 @@ impl acp_thread::AgentTelemetry for NativeAgentConnection {
}
}
struct NativeAgentSessionEditor {
struct NativeAgentSessionTruncate {
thread: Entity<Thread>,
acp_thread: WeakEntity<AcpThread>,
}
impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor {
impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate {
fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
match self.thread.update(cx, |thread, cx| {
thread.truncate(message_id.clone(), cx)?;
@@ -1104,6 +1112,66 @@ impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
}
}
pub struct AcpThreadEnvironment {
acp_thread: WeakEntity<AcpThread>,
}
impl ThreadEnvironment for AcpThreadEnvironment {
fn create_terminal(
&self,
command: String,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
cx: &mut AsyncApp,
) -> Task<Result<Rc<dyn TerminalHandle>>> {
let task = self.acp_thread.update(cx, |thread, cx| {
thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx)
});
let acp_thread = self.acp_thread.clone();
cx.spawn(async move |cx| {
let terminal = task?.await?;
let (drop_tx, drop_rx) = oneshot::channel();
let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?;
cx.spawn(async move |cx| {
drop_rx.await.ok();
acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx))
})
.detach();
let handle = AcpTerminalHandle {
terminal,
_drop_tx: Some(drop_tx),
};
Ok(Rc::new(handle) as _)
})
}
}
pub struct AcpTerminalHandle {
terminal: Entity<acp_thread::Terminal>,
_drop_tx: Option<oneshot::Sender<()>>,
}
impl TerminalHandle for AcpTerminalHandle {
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
self.terminal.read_with(cx, |term, _cx| term.id().clone())
}
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
self.terminal
.read_with(cx, |term, _cx| term.wait_for_exit())
}
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
self.terminal
.read_with(cx, |term, cx| term.current_output(cx))
}
}
#[cfg(test)]
mod tests {
use crate::HistoryEntryId;

View File

@@ -18,6 +18,7 @@ use sqlez::{
};
use std::sync::Arc;
use ui::{App, SharedString};
use zed_env_vars::ZED_STATELESS;
pub type DbMessage = crate::Message;
pub type DbSummary = DetailedSummaryState;
@@ -201,9 +202,6 @@ impl DbThread {
}
}
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType {
#[serde(rename = "json")]

View File

@@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_thinking(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -1349,7 +1348,6 @@ async fn test_cancellation(cx: &mut TestAppContext) {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -1688,7 +1686,6 @@ async fn test_truncate_second_message(cx: &mut TestAppContext) {
}
#[gpui::test]
#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows
async fn test_title_generation(cx: &mut TestAppContext) {
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
let fake_model = model.as_fake();
@@ -2353,15 +2350,20 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
settings::init(cx);
Project::init_settings(cx);
agent_settings::init(cx);
gpui_tokio::init(cx);
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
match model {
TestModel::Fake => {}
TestModel::Sonnet4 => {
gpui_tokio::init(cx);
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
}
};
watch_settings(fs.clone(), cx);
});
@@ -2475,6 +2477,7 @@ fn setup_context_server(
path: "somebinary".into(),
args: Vec::new(),
env: None,
timeout: None,
},
},
);

View File

@@ -45,14 +45,15 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::fmt::Write;
use std::{
collections::BTreeMap,
ops::RangeInclusive,
path::Path,
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use std::{fmt::Write, path::PathBuf};
use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
use uuid::Uuid;
@@ -523,6 +524,22 @@ pub enum AgentMessageContent {
ToolUse(LanguageModelToolUse),
}
pub trait TerminalHandle {
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
}
pub trait ThreadEnvironment {
fn create_terminal(
&self,
command: String,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
cx: &mut AsyncApp,
) -> Task<Result<Rc<dyn TerminalHandle>>>;
}
#[derive(Debug)]
pub enum ThreadEvent {
UserMessage(UserMessage),
@@ -535,6 +552,14 @@ pub enum ThreadEvent {
Stop(acp::StopReason),
}
#[derive(Debug)]
pub struct NewTerminal {
pub command: String,
pub output_byte_limit: Option<u64>,
pub cwd: Option<PathBuf>,
pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
}
#[derive(Debug)]
pub struct ToolCallAuthorization {
pub tool_call: acp::ToolCallUpdate,
@@ -1024,7 +1049,11 @@ impl Thread {
}
}
pub fn add_default_tools(&mut self, cx: &mut Context<Self>) {
pub fn add_default_tools(
&mut self,
environment: Rc<dyn ThreadEnvironment>,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
self.add_tool(CopyPathTool::new(self.project.clone()));
self.add_tool(CreateDirectoryTool::new(self.project.clone()));
@@ -1045,7 +1074,7 @@ impl Thread {
self.project.clone(),
self.action_log.clone(),
));
self.add_tool(TerminalTool::new(self.project.clone(), cx));
self.add_tool(TerminalTool::new(self.project.clone(), environment));
self.add_tool(ThinkingTool);
self.add_tool(WebSearchTool);
}
@@ -2389,19 +2418,6 @@ impl ToolCallEventStream {
.ok();
}
pub fn update_terminal(&self, terminal: Entity<acp_thread::Terminal>) {
self.stream
.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateTerminal {
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
terminal,
}
.into(),
)))
.ok();
}
pub fn authorize(&self, title: impl Into<String>, cx: &mut App) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));

View File

@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[schemars(inline)]
pub enum EditFileMode {
Edit,
Create,

View File

@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[schemars(inline)]
pub enum Timezone {
/// Use UTC for the datetime.
Utc,

View File

@@ -1,19 +1,19 @@
use agent_client_protocol as acp;
use anyhow::Result;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::{Project, terminals::TerminalKind};
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
use util::markdown::MarkdownInlineCode;
use crate::{AgentTool, ToolCallEventStream};
use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream};
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024;
/// Executes a shell one-liner and returns the combined output.
///
@@ -36,25 +36,14 @@ pub struct TerminalToolInput {
pub struct TerminalTool {
project: Entity<Project>,
determine_shell: Shared<Task<String>>,
environment: Rc<dyn ThreadEnvironment>,
}
impl TerminalTool {
pub fn new(project: Entity<Project>, cx: &mut App) -> Self {
let determine_shell = cx.background_spawn(async move {
if cfg!(windows) {
return get_system_shell();
}
if which::which("bash").is_ok() {
"bash".into()
} else {
get_system_shell()
}
});
pub fn new(project: Entity<Project>, environment: Rc<dyn ThreadEnvironment>) -> Self {
Self {
project,
determine_shell: determine_shell.shared(),
environment,
}
}
}
@@ -99,128 +88,49 @@ impl AgentTool for TerminalTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let language_registry = self.project.read(cx).languages().clone();
let working_dir = match working_dir(&input, &self.project, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)),
};
let program = self.determine_shell.clone();
let command = if cfg!(windows) {
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
} else if let Some(cwd) = working_dir
.as_ref()
.and_then(|cwd| cwd.as_os_str().to_str())
{
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", input.command)
} else {
format!("({}) </dev/null", input.command)
};
let args = vec!["-c".into(), command];
let env = match &working_dir {
Some(dir) => self.project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
let env = cx.spawn(async move |_| {
let mut env = env.await.unwrap_or_default();
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}
env
});
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.spawn(async move |cx| {
authorize.await?;
cx.spawn({
async move |cx| {
authorize.await?;
let terminal = self
.environment
.create_terminal(
input.command.clone(),
working_dir,
Some(COMMAND_OUTPUT_LIMIT),
cx,
)
.await?;
let program = program.await;
let env = env.await;
let terminal = self
.project
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
command: Some(program),
args,
cwd: working_dir.clone(),
env,
..Default::default()
}),
cx,
)
})?
.await?;
let acp_terminal = cx.new(|cx| {
acp_thread::Terminal::new(
input.command.clone(),
working_dir.clone(),
terminal.clone(),
language_registry,
cx,
)
})?;
event_stream.update_terminal(acp_terminal.clone());
let terminal_id = terminal.id(cx)?;
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
..Default::default()
});
let exit_status = terminal
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
(terminal.get_content(), terminal.total_lines())
})?;
let exit_status = terminal.wait_for_exit(cx)?.await;
let output = terminal.current_output(cx)?;
let (processed_content, finished_with_empty_output) = process_content(
&content,
&input.command,
exit_status.map(portable_pty::ExitStatus::from),
);
acp_terminal
.update(cx, |terminal, cx| {
terminal.finish(
exit_status,
content.len(),
processed_content.len(),
content_line_count,
finished_with_empty_output,
cx,
);
})
.log_err();
Ok(processed_content)
}
Ok(process_content(output, &input.command, exit_status))
})
}
}
fn process_content(
content: &str,
output: acp::TerminalOutputResponse,
command: &str,
exit_status: Option<portable_pty::ExitStatus>,
) -> (String, bool) {
let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
let content = if should_truncate {
let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len());
while !content.is_char_boundary(end_ix) {
end_ix -= 1;
}
// Don't truncate mid-line, clear the remainder of the last line
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
&content[..end_ix]
} else {
content
};
let content = content.trim();
exit_status: acp::TerminalExitStatus,
) -> String {
let content = output.output.trim();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let content = if should_truncate {
let content = if output.truncated {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
content.len(),
@@ -229,24 +139,21 @@ fn process_content(
content
};
let content = match exit_status {
Some(exit_status) if exit_status.success() => {
let content = match exit_status.exit_code {
Some(0) => {
if is_empty {
"Command executed successfully.".to_string()
} else {
content
}
}
Some(exit_status) => {
Some(exit_code) => {
if is_empty {
format!(
"Command \"{command}\" failed with exit code {}.",
exit_status.exit_code()
)
format!("Command \"{command}\" failed with exit code {}.", exit_code)
} else {
format!(
"Command \"{command}\" failed with exit code {}.\n\n{content}",
exit_status.exit_code()
exit_code
)
}
}
@@ -257,7 +164,7 @@ fn process_content(
)
}
};
(content, is_empty)
content
}
fn working_dir(
@@ -300,169 +207,3 @@ fn working_dir(
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
}
}
#[cfg(test)]
mod tests {
use agent_settings::AgentSettings;
use editor::EditorSettings;
use fs::RealFs;
use gpui::{BackgroundExecutor, TestAppContext};
use pretty_assertions::assert_eq;
use serde_json::json;
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::TerminalSettings;
use theme::ThemeSettings;
use util::test::TempTree;
use crate::ThreadEvent;
use super::*;
fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
zlog::init_test();
executor.allow_parking();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
ThemeSettings::register(cx);
TerminalSettings::register(cx);
EditorSettings::register(cx);
AgentSettings::register(cx);
});
}
#[gpui::test]
async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) {
if cfg!(windows) {
return;
}
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
let tree = TempTree::new(json!({
"project": {},
}));
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let input = TerminalToolInput {
command: "cat".to_owned(),
cd: tree
.path()
.join("project")
.as_path()
.to_string_lossy()
.to_string(),
};
let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test();
let result = cx
.update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx));
let auth = event_stream_rx.expect_authorization().await;
auth.response.send(auth.options[0].id.clone()).unwrap();
event_stream_rx.expect_terminal().await;
assert_eq!(result.await.unwrap(), "Command executed successfully.");
}
#[gpui::test]
async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) {
if cfg!(windows) {
return;
}
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
let tree = TempTree::new(json!({
"project": {},
"other-project": {},
}));
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let check = |input, expected, cx: &mut TestAppContext| {
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let result = cx.update(|cx| {
Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx)
});
cx.run_until_parked();
let event = stream_rx.try_next();
if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event {
auth.response.send(auth.options[0].id.clone()).unwrap();
}
cx.spawn(async move |_| {
let output = result.await;
assert_eq!(output.ok(), expected);
})
};
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("project").display()
)),
cx,
)
.await;
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
},
None, // other-project is a dir, but *not* a worktree (yet)
cx,
)
.await;
// Absolute path above the worktree root
check(
TerminalToolInput {
command: "pwd".into(),
cd: tree.path().to_string_lossy().into(),
},
None,
cx,
)
.await;
project
.update(cx, |project, cx| {
project.create_worktree(tree.path().join("other-project"), true, cx)
})
.await
.unwrap();
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("other-project").display()
)),
cx,
)
.await;
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
},
None,
cx,
)
.await;
}
}

View File

@@ -28,7 +28,7 @@ pub struct AcpConnection {
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
agent_capabilities: acp::AgentCapabilities,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
@@ -134,6 +134,7 @@ impl AcpConnection {
read_text_file: true,
write_text_file: true,
},
terminal: true,
},
})
.await?;
@@ -147,7 +148,7 @@ impl AcpConnection {
connection,
server_name,
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
agent_capabilities: response.agent_capabilities,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
@@ -155,7 +156,7 @@ impl AcpConnection {
}
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
&self.prompt_capabilities
&self.agent_capabilities.prompt_capabilities
}
}
@@ -222,7 +223,7 @@ impl AgentConnection for AcpConnection {
action_log,
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.prompt_capabilities),
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
cx,
)
})?;
@@ -344,11 +345,7 @@ impl acp::Client for ClientDelegate {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.session_thread(&arguments.session_id)?
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})??;
@@ -364,11 +361,7 @@ impl acp::Client for ClientDelegate {
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.session_thread(&arguments.session_id)?
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
@@ -382,16 +375,12 @@ impl acp::Client for ClientDelegate {
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
let task = self.session_thread(&arguments.session_id)?.update(
&mut self.cx.clone(),
|thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
})?;
},
)?;
let content = task.await?;
@@ -402,16 +391,92 @@ impl acp::Client for ClientDelegate {
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
self.session_thread(&notification.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
async fn create_terminal(
&self,
args: acp::CreateTerminalRequest,
) -> Result<acp::CreateTerminalResponse, acp::Error> {
let terminal = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.create_terminal(
args.command,
args.args,
args.env,
args.cwd,
args.output_byte_limit,
cx,
)
})?
.await?;
Ok(
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
terminal_id: terminal.id().clone(),
})?,
)
}
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.kill_terminal(args.terminal_id, cx)
})??;
Ok(())
}
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.release_terminal(args.terminal_id, cx)
})??;
Ok(())
}
async fn terminal_output(
&self,
args: acp::TerminalOutputRequest,
) -> Result<acp::TerminalOutputResponse, acp::Error> {
self.session_thread(&args.session_id)?
.read_with(&mut self.cx.clone(), |thread, cx| {
let out = thread
.terminal(args.terminal_id)?
.read(cx)
.current_output(cx);
Ok(out)
})?
}
async fn wait_for_terminal_exit(
&self,
args: acp::WaitForTerminalExitRequest,
) -> Result<acp::WaitForTerminalExitResponse, acp::Error> {
let exit_status = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit())
})??
.await;
Ok(acp::WaitForTerminalExitResponse { exit_status })
}
}
impl ClientDelegate {
fn session_thread(&self, session_id: &acp::SessionId) -> Result<WeakEntity<AcpThread>> {
let sessions = self.sessions.borrow();
sessions
.get(session_id)
.context("Failed to get session")
.map(|session| session.thread.clone())
}
}

View File

@@ -45,11 +45,20 @@ pub fn init(cx: &mut App) {
pub struct AgentServerDelegate {
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_available: Option<watch::Sender<Option<String>>>,
}
impl AgentServerDelegate {
pub fn new(project: Entity<Project>, status_tx: Option<watch::Sender<SharedString>>) -> Self {
Self { project, status_tx }
pub fn new(
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_tx: Option<watch::Sender<Option<String>>>,
) -> Self {
Self {
project,
status_tx,
new_version_available: new_version_tx,
}
}
pub fn project(&self) -> &Entity<Project> {
@@ -61,7 +70,7 @@ impl AgentServerDelegate {
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
ignore_system_version: bool,
search_path: bool,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
@@ -73,9 +82,10 @@ impl AgentServerDelegate {
)));
};
let status_tx = self.status_tx;
let new_version_available = self.new_version_available;
cx.spawn(async move |cx| {
if !ignore_system_version {
if search_path {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand {
path: bin,
@@ -101,9 +111,11 @@ impl AgentServerDelegate {
continue;
};
if let Some(version) = file_name
.to_str()
.and_then(|name| semver::Version::from_str(&name).ok())
if let Some(name) = file_name.to_str()
&& let Some(version) = semver::Version::from_str(name).ok()
&& fs
.is_file(&dir.join(file_name).join(&entrypoint_path))
.await
{
versions.push((version, file_name.to_owned()));
} else {
@@ -146,6 +158,7 @@ impl AgentServerDelegate {
cx.background_spawn({
let file_name = file_name.clone();
let dir = dir.clone();
let fs = fs.clone();
async move {
let latest_version =
node_runtime.npm_package_latest_version(&package_name).await;
@@ -160,6 +173,9 @@ impl AgentServerDelegate {
)
.await
.log_err();
if let Some(mut new_version_available) = new_version_available {
new_version_available.send(Some(latest_version)).ok();
}
}
}
})
@@ -171,7 +187,7 @@ impl AgentServerDelegate {
}
let dir = dir.clone();
cx.background_spawn(Self::download_latest_version(
fs,
fs.clone(),
dir.clone(),
node_runtime,
package_name,
@@ -179,14 +195,18 @@ impl AgentServerDelegate {
.await?
.into()
};
let agent_server_path = dir.join(version).join(entrypoint_path);
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
anyhow::ensure!(
agent_server_path_exists,
"Missing entrypoint path {} after installation",
agent_server_path.to_string_lossy()
);
anyhow::Ok(AgentServerCommand {
path: node_path,
args: vec![
dir.join(version)
.join(entrypoint_path)
.to_string_lossy()
.to_string(),
],
args: vec![agent_server_path.to_string_lossy().to_string()],
env: Default::default(),
})
})

View File

@@ -1,10 +1,9 @@
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
use settings::SettingsStore;
use std::path::Path;
use std::rc::Rc;
use std::{any::Any, path::PathBuf};
use anyhow::Result;
use anyhow::{Result, bail};
use gpui::{App, AppContext as _, SharedString, Task};
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
@@ -26,21 +25,24 @@ impl ClaudeCode {
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<ClaudeCodeLoginCommand>> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.get("claude")
.cloned()
});
cx.spawn(async move |cx| {
let mut command = if let Some(settings) = settings {
settings.command
let mut command = if custom_command.is_some() {
bail!("Cannot construct login command because a custom command was specified for claude-code-acp in settings")
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
true,
None,
false,
Some("0.2.5".parse().unwrap()),
cx,
)
})?
@@ -76,21 +78,25 @@ impl AgentServer for ClaudeCode {
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.get("claude")
.cloned()
});
cx.spawn(async move |cx| {
let mut command = if let Some(settings) = settings {
settings.command
let mut command = if let Some(custom_command) = custom_command {
custom_command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
true,
false,
None,
cx,
)
@@ -98,16 +104,17 @@ impl AgentServer for ClaudeCode {
.await?
};
if let Some(api_key) = cx
.update(AnthropicLanguageModelProvider::api_key)?
.await
.ok()
{
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(
root_dir_exists,
"Session root {} does not exist or is not a directory",
root_dir.to_string_lossy()
);
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
})

View File

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

View File

@@ -2,15 +2,13 @@ use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate};
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use settings::SettingsStore;
use crate::AllAgentServersSettings;
#[derive(Clone)]
pub struct Gemini;
@@ -36,27 +34,27 @@ impl AgentServer for Gemini {
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
let (custom_command, is_system) = cx.read_global(|settings: &SettingsStore, _| {
let s = settings.get::<AllAgentServersSettings>(None);
(
s.get("gemini").cloned(),
AllAgentServersSettings::is_system(s, "gemini"),
)
});
cx.spawn(async move |cx| {
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
let mut command = if let Some(custom_command) = custom_command
{
command
custom_command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
ignore_system_version,
is_system,
Some(Self::MINIMUM_VERSION.parse().unwrap()),
cx,
)
@@ -74,6 +72,13 @@ impl AgentServer for Gemini {
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(
root_dir_exists,
"Session root {} does not exist or is not a directory",
root_dir.to_string_lossy()
);
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
match &result {
Ok(connection) => {
@@ -92,7 +97,7 @@ impl AgentServer for Gemini {
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
@@ -129,7 +134,7 @@ impl AgentServer for Gemini {
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());

View File

@@ -6,20 +6,56 @@ use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<CustomAgentServerSettings>,
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettingsContent {
gemini: Option<GeminiSettingsContent>,
claude: Option<AgentServerCommand>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
pub custom: HashMap<SharedString, AgentServerCommand>,
}
#[derive(Clone, Debug, Default)]
pub struct AllAgentServersSettings {
pub commands: HashMap<SharedString, AgentServerCommand>,
pub gemini_is_system: bool,
}
impl AllAgentServersSettings {
pub fn is_system(this: &Self, name: &str) -> bool {
if name == "gemini" {
this.gemini_is_system
} else {
false
}
}
}
impl std::ops::Deref for AllAgentServersSettings {
type Target = HashMap<SharedString, AgentServerCommand>;
fn deref(&self) -> &Self::Target {
&self.commands
}
}
impl std::ops::DerefMut for AllAgentServersSettings {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.commands
}
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
pub struct GeminiSettingsContent {
ignore_system_version: Option<bool>,
#[serde(flatten)]
inner: Option<AgentServerCommand>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
@@ -69,37 +105,40 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct CustomAgentServerSettings {
pub struct AgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}
impl settings::Settings for AllAgentServersSettings {
const KEY: Option<&'static str> = Some("agent_servers");
type FileContent = Self;
type FileContent = AllAgentServersSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings {
for AllAgentServersSettingsContent {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if gemini.is_some() {
settings.gemini = gemini.clone();
if let Some(gemini) = gemini {
if let Some(ignore) = gemini.ignore_system_version {
settings.gemini_is_system = !ignore;
}
if let Some(gemini) = gemini.inner.as_ref() {
settings.insert("gemini".into(), gemini.clone());
}
}
if claude.is_some() {
settings.claude = claude.clone();
if let Some(claude) = claude.clone() {
settings.insert("claude".into(), claude);
}
// Merge custom agents
for (name, config) in custom {
for (name, command) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
settings.commands.insert(name.clone(), command.clone());
}
}
}
@@ -109,3 +148,31 @@ impl settings::Settings for AllAgentServersSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::{AgentServerCommand, GeminiSettingsContent};
#[test]
fn test_deserialization() {
let value = json!({
"command": "foo",
"args": ["bar"],
"ignore_system_version": false
});
let settings = serde_json::from_value::<GeminiSettingsContent>(value).unwrap();
assert_eq!(
settings,
GeminiSettingsContent {
ignore_system_version: Some(false),
inner: Some(AgentServerCommand {
path: "foo".into(),
args: vec!["bar".into()],
env: Default::default(),
})
}
)
}
}

View File

@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
#[settings_key(key = "agent", fallback_key = "assistant")]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -352,18 +353,19 @@ impl JsonSchema for LanguageModelProviderSetting {
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"enum": [
"anthropic",
"amazon-bedrock",
"google",
"lmstudio",
"ollama",
"openai",
"zed.dev",
"anthropic",
"copilot_chat",
"deepseek",
"openrouter",
"google",
"lmstudio",
"mistral",
"vercel"
"ollama",
"openai",
"openrouter",
"vercel",
"x_ai",
"zed.dev"
]
})
}
@@ -398,10 +400,6 @@ pub struct ContextServerPresetContent {
}
impl Settings for AgentSettings {
const KEY: Option<&'static str> = Some("agent");
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AgentSettingsContent;

View File

@@ -1,4 +1,4 @@
use std::cell::Cell;
use std::cell::{Cell, RefCell};
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
@@ -13,8 +13,10 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::CompletionDocumentation;
use project::{
Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
ProjectPath, Symbol, WorktreeId,
};
use prompt_store::PromptStore;
use rope::Point;
@@ -23,7 +25,7 @@ use ui::prelude::*;
use workspace::Workspace;
use crate::AgentPanel;
use crate::acp::message_editor::MessageEditor;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch;
@@ -67,6 +69,7 @@ pub struct ContextPickerCompletionProvider {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
}
impl ContextPickerCompletionProvider {
@@ -76,6 +79,7 @@ impl ContextPickerCompletionProvider {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
) -> Self {
Self {
message_editor,
@@ -83,6 +87,7 @@ impl ContextPickerCompletionProvider {
history_store,
prompt_store,
prompt_capabilities,
available_commands,
}
}
@@ -369,7 +374,42 @@ impl ContextPickerCompletionProvider {
})
}
fn search(
fn search_slash_commands(
&self,
query: String,
cx: &mut App,
) -> Task<Vec<acp::AvailableCommand>> {
let commands = self.available_commands.borrow().clone();
if commands.is_empty() {
return Task::ready(Vec::new());
}
cx.spawn(async move |cx| {
let candidates = commands
.iter()
.enumerate()
.map(|(id, command)| StringMatchCandidate::new(id, &command.name))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
true,
100,
&Arc::new(AtomicBool::default()),
cx.background_executor().clone(),
)
.await;
matches
.into_iter()
.map(|mat| commands[mat.candidate_id].clone())
.collect()
})
}
fn search_mentions(
&self,
mode: Option<ContextPickerMode>,
query: String,
@@ -651,10 +691,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next()?;
MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
ContextCompletion::try_parse(
line,
offset_to_line,
self.prompt_capabilities.get().embedded_context,
)
});
let Some(state) = state else {
@@ -667,97 +707,175 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let project = workspace.read(cx).project().clone();
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
let source_range = snapshot.anchor_before(state.source_range().start)
..snapshot.anchor_after(state.source_range().end);
let editor = self.message_editor.clone();
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let completions = cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
Match::File(FileMatch { mat, is_recent }) => {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
match state {
ContextCompletion::SlashCommand(SlashCommandCompletion {
command, argument, ..
}) => {
let search_task = self.search_slash_commands(command.unwrap_or_default(), cx);
cx.background_spawn(async move {
let completions = search_task
.await
.into_iter()
.map(|command| {
let new_text = if let Some(argument) = argument.as_ref() {
format!("/{} {}", command.name, argument)
} else {
format!("/{} ", command.name)
};
Self::completion_for_path(
project_path,
&mat.path_prefix,
is_recent,
mat.is_dir,
source_range.clone(),
editor.clone(),
project.clone(),
cx,
)
}
let is_missing_argument = argument.is_none() && command.input.is_some();
Completion {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(command.name.to_string(), None),
documentation: Some(CompletionDocumentation::MultiLinePlainText(
command.description.into(),
)),
source: project::CompletionSource::Custom,
icon_path: None,
insert_text_mode: None,
confirm: Some(Arc::new({
let editor = editor.clone();
move |intent, _window, cx| {
if !is_missing_argument {
cx.defer({
let editor = editor.clone();
move |cx| {
editor
.update(cx, |_editor, cx| {
match intent {
CompletionIntent::Complete
| CompletionIntent::CompleteWithInsert
| CompletionIntent::CompleteWithReplace => {
if !is_missing_argument {
cx.emit(MessageEditorEvent::Send);
}
}
CompletionIntent::Compose => {}
}
})
.ok();
}
});
}
is_missing_argument
}
})),
}
})
.collect();
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
symbol,
source_range.clone(),
editor.clone(),
workspace.clone(),
cx,
),
Ok(vec![CompletionResponse {
completions,
display_options: CompletionDisplayOptions {
dynamic_width: true,
},
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
ContextCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
let query = argument.unwrap_or_default();
let search_task =
self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
Match::Thread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
false,
editor.clone(),
cx,
)),
cx.spawn(async move |_, cx| {
let matches = search_task.await;
Match::RecentThread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
true,
editor.clone(),
cx,
)),
let completions = cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
Match::File(FileMatch { mat, is_recent }) => {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
source_range.clone(),
editor.clone(),
cx,
)),
Self::completion_for_path(
project_path,
&mat.path_prefix,
is_recent,
mat.is_dir,
source_range.clone(),
editor.clone(),
project.clone(),
cx,
)
}
Match::Fetch(url) => Self::completion_for_fetch(
source_range.clone(),
url,
editor.clone(),
cx,
),
Match::Symbol(SymbolMatch { symbol, .. }) => {
Self::completion_for_symbol(
symbol,
source_range.clone(),
editor.clone(),
workspace.clone(),
cx,
)
}
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
entry,
source_range.clone(),
editor.clone(),
&workspace,
cx,
),
})
.collect()
})?;
Match::Thread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
false,
editor.clone(),
cx,
)),
Ok(vec![CompletionResponse {
completions,
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
Match::RecentThread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
true,
editor.clone(),
cx,
)),
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
source_range.clone(),
editor.clone(),
cx,
)),
Match::Fetch(url) => Self::completion_for_fetch(
source_range.clone(),
url,
editor.clone(),
cx,
),
Match::Entry(EntryMatch { entry, .. }) => {
Self::completion_for_entry(
entry,
source_range.clone(),
editor.clone(),
&workspace,
cx,
)
}
})
.collect()
})?;
Ok(vec![CompletionResponse {
completions,
display_options: CompletionDisplayOptions {
dynamic_width: true,
},
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
}
}
fn is_completion_trigger(
@@ -775,14 +893,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
ContextCompletion::try_parse(
line,
offset_to_line,
self.prompt_capabilities.get().embedded_context,
)
.map(|completion| {
completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize
completion.source_range().start <= offset_to_line + position.column as usize
&& completion.source_range().end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else {
@@ -851,7 +969,7 @@ fn confirm_completion_callback(
.clone()
.update(cx, |message_editor, cx| {
message_editor
.confirm_completion(
.confirm_mention_completion(
crease_text,
start,
content_len,
@@ -867,6 +985,89 @@ fn confirm_completion_callback(
})
}
enum ContextCompletion {
SlashCommand(SlashCommandCompletion),
Mention(MentionCompletion),
}
impl ContextCompletion {
fn source_range(&self) -> Range<usize> {
match self {
Self::SlashCommand(completion) => completion.source_range.clone(),
Self::Mention(completion) => completion.source_range.clone(),
}
}
fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option<Self> {
if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) {
Some(Self::SlashCommand(command))
} else if let Some(mention) =
MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line)
{
Some(Self::Mention(mention))
} else {
None
}
}
}
#[derive(Debug, Default, PartialEq)]
pub struct SlashCommandCompletion {
pub source_range: Range<usize>,
pub command: Option<String>,
pub argument: Option<String>,
}
impl SlashCommandCompletion {
pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
// If we decide to support commands that are not at the beginning of the prompt, we can remove this check
if !line.starts_with('/') || offset_to_line != 0 {
return None;
}
let last_command_start = line.rfind('/')?;
if last_command_start >= line.len() {
return Some(Self::default());
}
if last_command_start > 0
&& line
.chars()
.nth(last_command_start - 1)
.is_some_and(|c| !c.is_whitespace())
{
return None;
}
let rest_of_line = &line[last_command_start + 1..];
let mut command = None;
let mut argument = None;
let mut end = last_command_start + 1;
if let Some(command_text) = rest_of_line.split_whitespace().next() {
command = Some(command_text.to_string());
end += command_text.len();
// Find the start of arguments after the command
if let Some(args_start) =
rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
{
let args = &rest_of_line[command_text.len() + args_start..].trim_end();
if !args.is_empty() {
argument = Some(args.to_string());
end += args.len() + 1;
}
}
}
Some(Self {
source_range: last_command_start + offset_to_line..end + offset_to_line,
command,
argument,
})
}
}
#[derive(Debug, Default, PartialEq)]
struct MentionCompletion {
source_range: Range<usize>,
@@ -932,6 +1133,62 @@ impl MentionCompletion {
mod tests {
use super::*;
#[test]
fn test_slash_command_completion_parse() {
assert_eq!(
SlashCommandCompletion::try_parse("/", 0),
Some(SlashCommandCompletion {
source_range: 0..1,
command: None,
argument: None,
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help", 0),
Some(SlashCommandCompletion {
source_range: 0..5,
command: Some("help".to_string()),
argument: None,
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help ", 0),
Some(SlashCommandCompletion {
source_range: 0..5,
command: Some("help".to_string()),
argument: None,
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help arg1", 0),
Some(SlashCommandCompletion {
source_range: 0..10,
command: Some("help".to_string()),
argument: Some("arg1".to_string()),
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help arg1 arg2", 0),
Some(SlashCommandCompletion {
source_range: 0..15,
command: Some("help".to_string()),
argument: Some("arg1 arg2".to_string()),
})
);
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
}
#[test]
fn test_mention_completion_parse() {
assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);

View File

@@ -1,13 +1,17 @@
use std::{cell::Cell, ops::Range, rc::Rc};
use std::{
cell::{Cell, RefCell},
ops::Range,
rc::Rc,
};
use acp_thread::{AcpThread, AgentThreadEntry};
use agent_client_protocol::{PromptCapabilities, ToolCallId};
use agent_client_protocol::{self as acp, ToolCallId};
use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use project::Project;
@@ -26,8 +30,9 @@ pub struct EntryViewState {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
}
impl EntryViewState {
@@ -36,8 +41,9 @@ impl EntryViewState {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
) -> Self {
Self {
workspace,
@@ -45,8 +51,9 @@ impl EntryViewState {
history_store,
prompt_store,
entries: Vec::new(),
prevent_slash_commands,
prompt_capabilities,
available_commands,
agent_name,
}
}
@@ -85,8 +92,9 @@ impl EntryViewState {
self.history_store.clone(),
self.prompt_store.clone(),
self.prompt_capabilities.clone(),
self.available_commands.clone(),
self.agent_name.clone(),
"Edit message @ to include context",
self.prevent_slash_commands,
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
@@ -125,22 +133,35 @@ impl EntryViewState {
views
};
let is_tool_call_completed =
matches!(tool_call.status, acp_thread::ToolCallStatus::Completed);
for terminal in terminals {
views.entry(terminal.entity_id()).or_insert_with(|| {
let element = create_terminal(
self.workspace.clone(),
self.project.clone(),
terminal.clone(),
window,
cx,
)
.into_any();
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::NewTerminal(id.clone()),
});
element
});
match views.entry(terminal.entity_id()) {
collections::hash_map::Entry::Vacant(entry) => {
let element = create_terminal(
self.workspace.clone(),
self.project.clone(),
terminal.clone(),
window,
cx,
)
.into_any();
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::NewTerminal(id.clone()),
});
entry.insert(element);
}
collections::hash_map::Entry::Occupied(_entry) => {
if is_tool_call_completed && terminal.read(cx).output().is_none() {
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::TerminalMovedToBackground(id.clone()),
});
}
}
}
}
for diff in diffs {
@@ -186,7 +207,7 @@ impl EntryViewState {
self.entries.drain(range);
}
pub fn settings_changed(&mut self, cx: &mut App) {
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
@@ -217,6 +238,7 @@ pub struct EntryViewEvent {
pub enum ViewEvent {
NewDiff(ToolCallId),
NewTerminal(ToolCallId),
TerminalMovedToBackground(ToolCallId),
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
}
@@ -457,7 +479,8 @@ mod tests {
history_store,
None,
Default::default(),
false,
Default::default(),
"Test Agent".into(),
)
});

View File

@@ -1,5 +1,5 @@
use crate::{
acp::completion_provider::ContextPickerCompletionProvider,
acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
};
use acp_thread::{MentionUri, selection_name};
@@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path;
use collections::{HashMap, HashSet};
use editor::{
Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, MultiBuffer,
SemanticsProvider, ToOffset,
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId,
MultiBuffer, ToOffset,
actions::Paste,
display_map::{Crease, CreaseId, FoldId},
display_map::{Crease, CreaseId, FoldId, Inlay},
};
use futures::{
FutureExt as _,
@@ -22,18 +22,20 @@ use futures::{
};
use gpui::{
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
EventEmitter, FocusHandle, Focusable, HighlightStyle, Image, ImageFormat, Img, KeyContext,
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity, pulsating_between,
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
};
use language::{Buffer, Language};
use language::{Buffer, Language, language_settings::InlayHintKind};
use language_model::LanguageModelImage;
use postage::stream::Stream as _;
use project::{CompletionIntent, Project, ProjectItem, ProjectPath, Worktree};
use project::{
CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree,
};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::Settings;
use std::{
cell::Cell,
cell::{Cell, RefCell},
ffi::OsStr,
fmt::Write,
ops::{Range, RangeInclusive},
@@ -42,20 +44,18 @@ use std::{
sync::Arc,
time::Duration,
};
use text::{OffsetRangeExt, ToOffset as _};
use text::OffsetRangeExt;
use theme::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, App, ButtonCommon, ButtonLike, ButtonStyle, Color, Element as _,
FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, SharedString, Styled,
TextSize, TintColor, Toggleable, Window, div, h_flex, px,
LabelCommon, LabelSize, ParentElement, Render, SelectableButton, Styled, TextSize, TintColor,
Toggleable, Window, div, h_flex,
};
use util::{ResultExt, debug_panic};
use workspace::{Workspace, notifications::NotifyResultExt as _};
use zed_actions::agent::Chat;
const PARSE_SLASH_COMMAND_DEBOUNCE: Duration = Duration::from_millis(50);
pub struct MessageEditor {
mention_set: MentionSet,
editor: Entity<Editor>,
@@ -63,8 +63,9 @@ pub struct MessageEditor {
workspace: WeakEntity<Workspace>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
_subscriptions: Vec<Subscription>,
_parse_slash_command_task: Task<()>,
}
@@ -79,6 +80,8 @@ pub enum MessageEditorEvent {
impl EventEmitter<MessageEditorEvent> for MessageEditor {}
const COMMAND_HINT_INLAY_ID: usize = 0;
impl MessageEditor {
pub fn new(
workspace: WeakEntity<Workspace>,
@@ -86,8 +89,9 @@ impl MessageEditor {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
placeholder: impl Into<Arc<str>>,
prevent_slash_commands: bool,
mode: EditorMode,
window: &mut Window,
cx: &mut Context<Self>,
@@ -99,16 +103,14 @@ impl MessageEditor {
},
None,
);
let completion_provider = ContextPickerCompletionProvider::new(
let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
cx.weak_entity(),
workspace.clone(),
history_store.clone(),
prompt_store.clone(),
prompt_capabilities.clone(),
);
let semantics_provider = Rc::new(SlashCommandSemanticsProvider {
range: Cell::new(None),
});
available_commands.clone(),
));
let mention_set = MentionSet::default();
let editor = cx.new(|cx| {
let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
@@ -119,15 +121,12 @@ impl MessageEditor {
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);
editor.set_completion_provider(Some(Rc::new(completion_provider)));
editor.set_completion_provider(Some(completion_provider.clone()));
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
placement: Some(ContextMenuPlacement::Above),
});
if prevent_slash_commands {
editor.set_semantics_provider(Some(semantics_provider.clone()));
}
editor.register_addon(MessageEditorAddon::new());
editor
});
@@ -141,21 +140,33 @@ impl MessageEditor {
})
.detach();
let mut has_hint = false;
let mut subscriptions = Vec::new();
subscriptions.push(cx.subscribe_in(&editor, window, {
let semantics_provider = semantics_provider.clone();
move |this, editor, event, window, cx| {
if let EditorEvent::Edited { .. } = event {
if prevent_slash_commands {
this.highlight_slash_command(
semantics_provider.clone(),
editor.clone(),
window,
let snapshot = editor.update(cx, |editor, cx| {
let new_hints = this
.command_hint(editor.buffer(), cx)
.into_iter()
.collect::<Vec<_>>();
let has_new_hint = !new_hints.is_empty();
editor.splice_inlays(
if has_hint {
&[InlayId::Hint(COMMAND_HINT_INLAY_ID)]
} else {
&[]
},
new_hints,
cx,
);
}
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
has_hint = has_new_hint;
editor.snapshot(window, cx)
});
this.mention_set.remove_invalid(snapshot);
cx.notify();
}
}
@@ -168,13 +179,57 @@ impl MessageEditor {
workspace,
history_store,
prompt_store,
prevent_slash_commands,
prompt_capabilities,
available_commands,
agent_name,
_subscriptions: subscriptions,
_parse_slash_command_task: Task::ready(()),
}
}
fn command_hint(&self, buffer: &Entity<MultiBuffer>, cx: &App) -> Option<Inlay> {
let available_commands = self.available_commands.borrow();
if available_commands.is_empty() {
return None;
}
let snapshot = buffer.read(cx).snapshot(cx);
let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?;
if parsed_command.argument.is_some() {
return None;
}
let command_name = parsed_command.command?;
let available_command = available_commands
.iter()
.find(|command| command.name == command_name)?;
let acp::AvailableCommandInput::Unstructured { mut hint } =
available_command.input.clone()?;
let mut hint_pos = parsed_command.source_range.end + 1;
if hint_pos > snapshot.len() {
hint_pos = snapshot.len();
hint.insert(0, ' ');
}
let hint_pos = snapshot.anchor_after(hint_pos);
Some(Inlay::hint(
COMMAND_HINT_INLAY_ID,
hint_pos,
&InlayHint {
position: hint_pos.text_anchor,
label: InlayHintLabel::String(hint),
kind: Some(InlayHintKind::Parameter),
padding_left: false,
padding_right: false,
tooltip: None,
resolve_state: project::ResolveState::Resolved,
},
))
}
pub fn insert_thread_summary(
&mut self,
thread: agent2::DbThreadMetadata,
@@ -191,7 +246,7 @@ impl MessageEditor {
.text_anchor
});
self.confirm_completion(
self.confirm_mention_completion(
thread.title.clone(),
start,
thread.title.len(),
@@ -227,7 +282,7 @@ impl MessageEditor {
.collect()
}
pub fn confirm_completion(
pub fn confirm_mention_completion(
&mut self,
crease_text: SharedString,
start: text::Anchor,
@@ -645,7 +700,7 @@ impl MessageEditor {
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
let delegate = AgentServerDelegate::new(self.project.clone(), None);
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
let connection = server.connect(Path::new(""), delegate, cx);
cx.spawn(async move |_, cx| {
let agent = connection.await?;
@@ -679,21 +734,62 @@ impl MessageEditor {
})
}
fn validate_slash_commands(
text: &str,
available_commands: &[acp::AvailableCommand],
agent_name: &str,
) -> Result<()> {
if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) {
if let Some(command_name) = parsed_command.command {
// Check if this command is in the list of available commands from the server
let is_supported = available_commands
.iter()
.any(|cmd| cmd.name == command_name);
if !is_supported {
return Err(anyhow!(
"The /{} command is not supported by {}.\n\nAvailable commands: {}",
command_name,
agent_name,
if available_commands.is_empty() {
"none".to_string()
} else {
available_commands
.iter()
.map(|cmd| format!("/{}", cmd.name))
.collect::<Vec<_>>()
.join(", ")
}
));
}
}
}
Ok(())
}
pub fn contents(
&self,
cx: &mut Context<Self>,
) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
// Check for unsupported slash commands before spawning async task
let text = self.editor.read(cx).text(cx);
let available_commands = self.available_commands.borrow().clone();
if let Err(err) =
Self::validate_slash_commands(&text, &available_commands, &self.agent_name)
{
return Task::ready(Err(err));
}
let contents = self
.mention_set
.contents(&self.prompt_capabilities.get(), cx);
let editor = self.editor.clone();
let prevent_slash_commands = self.prevent_slash_commands;
cx.spawn(async move |_, cx| {
let contents = contents.await?;
let mut all_tracked_buffers = Vec::new();
editor.update(cx, |editor, cx| {
let result = editor.update(cx, |editor, cx| {
let mut ix = 0;
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let text = editor.text(cx);
@@ -706,14 +802,16 @@ impl MessageEditor {
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
let chunk = if prevent_slash_commands
&& ix == 0
&& parse_slash_command(&text[ix..]).is_some()
{
format!(" {}", &text[ix..crease_range.start]).into()
} else {
text[ix..crease_range.start].into()
};
//todo(): Custom slash command ContentBlock?
// let chunk = if prevent_slash_commands
// && ix == 0
// && parse_slash_command(&text[ix..]).is_some()
// {
// format!(" {}", &text[ix..crease_range.start]).into()
// } else {
// text[ix..crease_range.start].into()
// };
let chunk = text[ix..crease_range.start].into();
chunks.push(chunk);
}
let chunk = match mention {
@@ -769,22 +867,24 @@ impl MessageEditor {
}
if ix < text.len() {
let last_chunk = if prevent_slash_commands
&& ix == 0
&& parse_slash_command(&text[ix..]).is_some()
{
format!(" {}", text[ix..].trim_end())
} else {
text[ix..].trim_end().to_owned()
};
//todo(): Custom slash command ContentBlock?
// let last_chunk = if prevent_slash_commands
// && ix == 0
// && parse_slash_command(&text[ix..]).is_some()
// {
// format!(" {}", text[ix..].trim_end())
// } else {
// text[ix..].trim_end().to_owned()
// };
let last_chunk = text[ix..].trim_end().to_owned();
if !last_chunk.is_empty() {
chunks.push(last_chunk.into());
}
}
});
(chunks, all_tracked_buffers)
})
Ok((chunks, all_tracked_buffers))
})?;
result
})
}
@@ -971,7 +1071,14 @@ impl MessageEditor {
cx,
);
});
tasks.push(self.confirm_completion(file_name, anchor, content_len, uri, window, cx));
tasks.push(self.confirm_mention_completion(
file_name,
anchor,
content_len,
uri,
window,
cx,
));
}
cx.spawn(async move |_, _| {
join_all(tasks).await;
@@ -1133,48 +1240,6 @@ impl MessageEditor {
cx.notify();
}
fn highlight_slash_command(
&mut self,
semantics_provider: Rc<SlashCommandSemanticsProvider>,
editor: Entity<Editor>,
window: &mut Window,
cx: &mut Context<Self>,
) {
struct InvalidSlashCommand;
self._parse_slash_command_task = cx.spawn_in(window, async move |_, cx| {
cx.background_executor()
.timer(PARSE_SLASH_COMMAND_DEBOUNCE)
.await;
editor
.update_in(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let range = parse_slash_command(&editor.text(cx));
semantics_provider.range.set(range);
if let Some((start, end)) = range {
editor.highlight_text::<InvalidSlashCommand>(
vec![
snapshot.buffer_snapshot.anchor_after(start)
..snapshot.buffer_snapshot.anchor_before(end),
],
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.),
color: Some(gpui::red()),
wavy: true,
}),
..Default::default()
},
cx,
);
} else {
editor.clear_highlights::<InvalidSlashCommand>(cx);
}
})
.ok();
})
}
pub fn text(&self, cx: &App) -> String {
self.editor.read(cx).text(cx)
}
@@ -1234,6 +1299,7 @@ impl Render for MessageEditor {
local_player: cx.theme().players().local(),
text: text_style,
syntax: cx.theme().syntax().clone(),
inlay_hints_style: editor::make_inlay_hints_style(cx),
..Default::default()
},
)
@@ -1264,7 +1330,7 @@ pub(crate) fn insert_crease_for_mention(
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(
render: render_mention_fold_button(
crease_label,
crease_icon,
start..end,
@@ -1294,7 +1360,7 @@ pub(crate) fn insert_crease_for_mention(
Some((crease_id, tx))
}
fn render_fold_icon_button(
fn render_mention_fold_button(
label: SharedString,
icon: SharedString,
range: Range<Anchor>,
@@ -1471,118 +1537,6 @@ impl MentionSet {
}
}
struct SlashCommandSemanticsProvider {
range: Cell<Option<(usize, usize)>>,
}
impl SemanticsProvider for SlashCommandSemanticsProvider {
fn hover(
&self,
buffer: &Entity<Buffer>,
position: text::Anchor,
cx: &mut App,
) -> Option<Task<Vec<project::Hover>>> {
let snapshot = buffer.read(cx).snapshot();
let offset = position.to_offset(&snapshot);
let (start, end) = self.range.get()?;
if !(start..end).contains(&offset) {
return None;
}
let range = snapshot.anchor_after(start)..snapshot.anchor_after(end);
Some(Task::ready(vec![project::Hover {
contents: vec![project::HoverBlock {
text: "Slash commands are not supported".into(),
kind: project::HoverBlockKind::PlainText,
}],
range: Some(range),
language: None,
}]))
}
fn inline_values(
&self,
_buffer_handle: Entity<Buffer>,
_range: Range<text::Anchor>,
_cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
None
}
fn inlay_hints(
&self,
_buffer_handle: Entity<Buffer>,
_range: Range<text::Anchor>,
_cx: &mut App,
) -> Option<Task<anyhow::Result<Vec<project::InlayHint>>>> {
None
}
fn resolve_inlay_hint(
&self,
_hint: project::InlayHint,
_buffer_handle: Entity<Buffer>,
_server_id: lsp::LanguageServerId,
_cx: &mut App,
) -> Option<Task<anyhow::Result<project::InlayHint>>> {
None
}
fn supports_inlay_hints(&self, _buffer: &Entity<Buffer>, _cx: &mut App) -> bool {
false
}
fn document_highlights(
&self,
_buffer: &Entity<Buffer>,
_position: text::Anchor,
_cx: &mut App,
) -> Option<Task<Result<Vec<project::DocumentHighlight>>>> {
None
}
fn definitions(
&self,
_buffer: &Entity<Buffer>,
_position: text::Anchor,
_kind: editor::GotoDefinitionKind,
_cx: &mut App,
) -> Option<Task<Result<Vec<project::LocationLink>>>> {
None
}
fn range_for_rename(
&self,
_buffer: &Entity<Buffer>,
_position: text::Anchor,
_cx: &mut App,
) -> Option<Task<Result<Option<Range<text::Anchor>>>>> {
None
}
fn perform_rename(
&self,
_buffer: &Entity<Buffer>,
_position: text::Anchor,
_new_name: String,
_cx: &mut App,
) -> Option<Task<Result<project::ProjectTransaction>>> {
None
}
}
fn parse_slash_command(text: &str) -> Option<(usize, usize)> {
if let Some(remainder) = text.strip_prefix('/') {
let pos = remainder
.find(char::is_whitespace)
.unwrap_or(remainder.len());
let command = &remainder[..pos];
if !command.is_empty() && command.chars().all(char::is_alphanumeric) {
return Some((0, 1 + command.len()));
}
}
None
}
pub struct MessageEditorAddon {}
impl MessageEditorAddon {
@@ -1610,7 +1564,13 @@ impl Addon for MessageEditorAddon {
#[cfg(test)]
mod tests {
use std::{cell::Cell, ops::Range, path::Path, rc::Rc, sync::Arc};
use std::{
cell::{Cell, RefCell},
ops::Range,
path::Path,
rc::Rc,
sync::Arc,
};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
@@ -1657,8 +1617,9 @@ mod tests {
history_store.clone(),
None,
Default::default(),
Default::default(),
"Test Agent".into(),
"Test",
false,
EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
@@ -1735,6 +1696,140 @@ mod tests {
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
}
#[gpui::test]
async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/test",
json!({
".zed": {
"tasks.json": r#"[{"label": "test", "command": "echo"}]"#
},
"src": {
"main.rs": "fn main() {}",
},
}),
)
.await;
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
// Start with no available commands - simulating Claude which doesn't support slash commands
let available_commands = Rc::new(RefCell::new(vec![]));
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace_handle = workspace.downgrade();
let message_editor = workspace.update_in(cx, |_, window, cx| {
cx.new(|cx| {
MessageEditor::new(
workspace_handle.clone(),
project.clone(),
history_store.clone(),
None,
prompt_capabilities.clone(),
available_commands.clone(),
"Claude Code".into(),
"Test",
EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
},
window,
cx,
)
})
});
let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
// Test that slash commands fail when no available_commands are set (empty list means no commands supported)
editor.update_in(cx, |editor, window, cx| {
editor.set_text("/file test.txt", window, cx);
});
let contents_result = message_editor
.update(cx, |message_editor, cx| message_editor.contents(cx))
.await;
// Should fail because available_commands is empty (no commands supported)
assert!(contents_result.is_err());
let error_message = contents_result.unwrap_err().to_string();
assert!(error_message.contains("not supported by Claude Code"));
assert!(error_message.contains("Available commands: none"));
// Now simulate Claude providing its list of available commands (which doesn't include file)
available_commands.replace(vec![acp::AvailableCommand {
name: "help".to_string(),
description: "Get help".to_string(),
input: None,
}]);
// Test that unsupported slash commands trigger an error when we have a list of available commands
editor.update_in(cx, |editor, window, cx| {
editor.set_text("/file test.txt", window, cx);
});
let contents_result = message_editor
.update(cx, |message_editor, cx| message_editor.contents(cx))
.await;
assert!(contents_result.is_err());
let error_message = contents_result.unwrap_err().to_string();
assert!(error_message.contains("not supported by Claude Code"));
assert!(error_message.contains("/file"));
assert!(error_message.contains("Available commands: /help"));
// Test that supported commands work fine
editor.update_in(cx, |editor, window, cx| {
editor.set_text("/help", window, cx);
});
let contents_result = message_editor
.update(cx, |message_editor, cx| message_editor.contents(cx))
.await;
// Should succeed because /help is in available_commands
assert!(contents_result.is_ok());
// Test that regular text works fine
editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello Claude!", window, cx);
});
let (content, _) = message_editor
.update(cx, |message_editor, cx| message_editor.contents(cx))
.await
.unwrap();
assert_eq!(content.len(), 1);
if let acp::ContentBlock::Text(text) = &content[0] {
assert_eq!(text.text, "Hello Claude!");
} else {
panic!("Expected ContentBlock::Text");
}
// Test that @ mentions still work
editor.update_in(cx, |editor, window, cx| {
editor.set_text("Check this @", window, cx);
});
// The @ mention functionality should not be affected
let (content, _) = message_editor
.update(cx, |message_editor, cx| message_editor.contents(cx))
.await
.unwrap();
assert_eq!(content.len(), 1);
if let acp::ContentBlock::Text(text) = &content[0] {
assert_eq!(text.text, "Check this @");
} else {
panic!("Expected ContentBlock::Text");
}
}
struct MessageEditorItem(Entity<MessageEditor>);
impl Item for MessageEditorItem {
@@ -1764,7 +1859,192 @@ mod tests {
}
#[gpui::test]
async fn test_context_completion_provider(cx: &mut TestAppContext) {
async fn test_completion_provider_commands(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
cx.update(|cx| {
language::init(cx);
editor::init(cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});
let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let workspace = window.root(cx).unwrap();
let mut cx = VisualTestContext::from_window(*window, cx);
let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(context_store, cx));
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![
acp::AvailableCommand {
name: "quick-math".to_string(),
description: "2 + 2 = 4 - 1 = 3".to_string(),
input: None,
},
acp::AvailableCommand {
name: "say-hello".to_string(),
description: "Say hello to whoever you want".to_string(),
input: Some(acp::AvailableCommandInput::Unstructured {
hint: "<name>".to_string(),
}),
},
]));
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
let message_editor = cx.new(|cx| {
MessageEditor::new(
workspace_handle,
project.clone(),
history_store.clone(),
None,
prompt_capabilities.clone(),
available_commands.clone(),
"Test Agent".into(),
"Test",
EditorMode::AutoHeight {
max_lines: None,
min_lines: 1,
},
window,
cx,
)
});
workspace.active_pane().update(cx, |pane, cx| {
pane.add_item(
Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
true,
true,
None,
window,
cx,
);
});
message_editor.read(cx).focus_handle(cx).focus(window);
message_editor.read(cx).editor().clone()
});
cx.simulate_input("/");
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[
("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()),
("say-hello".into(), "Say hello to whoever you want".into())
]
);
editor.set_text("", window, cx);
});
cx.simulate_input("/qui");
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/qui");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())]
);
editor.set_text("", window, cx);
});
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.display_text(cx), "/quick-math ");
assert!(!editor.has_visible_completions_menu());
editor.set_text("", window, cx);
});
cx.simulate_input("/say");
editor.update_in(&mut cx, |editor, _window, cx| {
assert_eq!(editor.display_text(cx), "/say");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[("say-hello".into(), "Say hello to whoever you want".into())]
);
});
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, _window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
assert_eq!(editor.display_text(cx), "/say-hello <name>");
assert!(editor.has_visible_completions_menu());
assert_eq!(
current_completion_labels_with_documentation(editor),
&[("say-hello".into(), "Say hello to whoever you want".into())]
);
});
cx.simulate_input("GPT5");
editor.update_in(&mut cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/say-hello GPT5");
assert_eq!(editor.display_text(cx), "/say-hello GPT5");
assert!(!editor.has_visible_completions_menu());
// Delete argument
for _ in 0..4 {
editor.backspace(&editor::actions::Backspace, window, cx);
}
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, window, cx| {
assert_eq!(editor.text(cx), "/say-hello ");
// Hint is visible because argument was deleted
assert_eq!(editor.display_text(cx), "/say-hello <name>");
// Delete last command letter
editor.backspace(&editor::actions::Backspace, window, cx);
editor.backspace(&editor::actions::Backspace, window, cx);
});
cx.run_until_parked();
editor.update_in(&mut cx, |editor, _window, cx| {
// Hint goes away once command no longer matches an available one
assert_eq!(editor.text(cx), "/say-hell");
assert_eq!(editor.display_text(cx), "/say-hell");
assert!(!editor.has_visible_completions_menu());
});
}
#[gpui::test]
async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) {
init_test(cx);
let app_state = cx.update(AppState::test);
@@ -1857,8 +2137,9 @@ mod tests {
history_store.clone(),
None,
prompt_capabilities.clone(),
Default::default(),
"Test Agent".into(),
"Test",
false,
EditorMode::AutoHeight {
max_lines: None,
min_lines: 1,
@@ -1888,7 +2169,6 @@ mod tests {
assert_eq!(editor.text(cx), "Lorem @");
assert!(editor.has_visible_completions_menu());
// Only files since we have default capabilities
assert_eq!(
current_completion_labels(editor),
&[
@@ -2128,7 +2408,7 @@ mod tests {
lsp::SymbolInformation {
name: "MySymbol".into(),
location: lsp::Location {
uri: lsp::Url::from_file_path(path!("/dir/a/one.txt")).unwrap(),
uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(),
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 1),
@@ -2284,4 +2564,20 @@ mod tests {
.map(|completion| completion.label.text)
.collect::<Vec<_>>()
}
fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> {
let completions = editor.current_completions().expect("Missing completions");
completions
.into_iter()
.map(|completion| {
(
completion.label.text,
completion
.documentation
.map(|d| d.text().to_string())
.unwrap_or_default(),
)
})
.collect::<Vec<_>>()
}
}

View File

@@ -71,7 +71,7 @@ impl AcpModelPickerDelegate {
let (models, selected_model) = futures::join!(models_task, selected_model_task);
this.update_in(cx, |this, window, cx| {
this.delegate.models = models.log_err();
this.delegate.models = models.ok();
this.delegate.selected_model = selected_model.ok();
this.refresh(window, cx)
})
@@ -141,11 +141,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
cx.spawn_in(window, async move |this, cx| {
let filtered_models = match this
.read_with(cx, |this, cx| {
if let Some(models) = this.delegate.models.as_ref() {
log::debug!("Filtering {} models.", models.len());
} else {
log::debug!("No models available.");
}
this.delegate.models.clone().map(move |models| {
fuzzy_search(models, query, cx.background_executor().clone())
})
@@ -157,8 +152,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
None => AgentModelList::Flat(vec![]),
};
log::debug!("Filtered models. {} available.", filtered_models.len());
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models).collect();

View File

@@ -36,6 +36,14 @@ impl AcpModelSelectorPopover {
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
self.menu_handle.toggle(window, cx);
}
pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
self.selector
.read(cx)
.delegate
.active_model()
.map(|model| model.name.clone())
}
}
impl Render for AcpModelSelectorPopover {

View File

@@ -8,7 +8,7 @@ use action_log::ActionLog;
use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use anyhow::{Context as _, Result, anyhow, bail};
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
@@ -23,9 +23,9 @@ use gpui::{
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
point, prelude::*, pulsating_between,
Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*,
pulsating_between,
};
use language::Buffer;
@@ -35,7 +35,7 @@ use project::{Project, ProjectEntryId};
use prompt_store::{PromptId, PromptStore};
use rope::Point;
use settings::{Settings as _, SettingsStore};
use std::cell::Cell;
use std::cell::{Cell, RefCell};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
@@ -43,10 +43,10 @@ use std::{collections::BTreeMap, rc::Rc, time::Duration};
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::ThemeSettings;
use theme::{AgentFontSize, ThemeSettings};
use ui::{
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
@@ -78,10 +78,12 @@ enum ThreadFeedback {
Negative,
}
#[derive(Debug)]
enum ThreadError {
PaymentRequired,
ModelRequestLimitReached(cloud_llm_client::Plan),
ToolUseLimitReached,
Refusal,
AuthenticationRequired(SharedString),
Other(SharedString),
}
@@ -284,9 +286,11 @@ pub struct AcpThreadView {
should_be_following: bool,
editing_message: Option<usize>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
is_loading_contents: bool,
new_server_version_available: Option<SharedString>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 3],
_subscriptions: [Subscription; 4],
}
enum ThreadState {
@@ -325,10 +329,15 @@ impl AcpThreadView {
cx: &mut Context<Self>,
) -> Self {
let prompt_capabilities = Rc::new(Cell::new(acp::PromptCapabilities::default()));
let prevent_slash_commands = agent.clone().downcast::<ClaudeCode>().is_some();
let available_commands = Rc::new(RefCell::new(vec![]));
let placeholder = if agent.name() == "Zed Agent" {
format!("Message the {} — @ to include context", agent.name())
} else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
format!(
"Message {} — @ to include context, / for commands",
agent.name()
)
} else {
format!("Message {} — @ to include context", agent.name())
};
@@ -340,8 +349,9 @@ impl AcpThreadView {
history_store.clone(),
prompt_store.clone(),
prompt_capabilities.clone(),
available_commands.clone(),
agent.name(),
placeholder,
prevent_slash_commands,
editor::EditorMode::AutoHeight {
min_lines: MIN_EDITOR_LINES,
max_lines: Some(MAX_EDITOR_LINES),
@@ -364,12 +374,14 @@ impl AcpThreadView {
history_store.clone(),
prompt_store.clone(),
prompt_capabilities.clone(),
prevent_slash_commands,
available_commands.clone(),
agent.name(),
)
});
let subscriptions = [
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
];
@@ -396,18 +408,34 @@ impl AcpThreadView {
editing_message: None,
edits_expanded: false,
plan_expanded: false,
prompt_capabilities,
available_commands,
editor_expanded: false,
should_be_following: false,
history_store,
hovered_recent_history_item: None,
prompt_capabilities,
is_loading_contents: false,
_subscriptions: subscriptions,
_cancel_task: None,
focus_handle: cx.focus_handle(),
new_server_version_available: None,
}
}
fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.thread_state = Self::initial_state(
self.agent.clone(),
None,
self.workspace.clone(),
self.project.clone(),
window,
cx,
);
self.available_commands.replace(vec![]);
self.new_server_version_available.take();
cx.notify();
}
fn initial_state(
agent: Rc<dyn AgentServer>,
resume_thread: Option<DbThreadMetadata>,
@@ -416,14 +444,37 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) -> ThreadState {
let root_dir = project
.read(cx)
.visible_worktrees(cx)
if !project.read(cx).is_local() && agent.clone().downcast::<NativeAgentServer>().is_none() {
return ThreadState::LoadError(LoadError::Other(
"External agents are not yet supported for remote projects.".into(),
));
}
let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
// Pick the first non-single-file worktree for the root directory if there are any,
// and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
worktrees.sort_by(|l, r| {
l.read(cx)
.is_single_file()
.cmp(&r.read(cx).is_single_file())
});
let root_dir = worktrees
.into_iter()
.filter_map(|worktree| {
if worktree.read(cx).is_single_file() {
Some(worktree.read(cx).abs_path().parent()?.into())
} else {
Some(worktree.read(cx).abs_path())
}
})
.next()
.map(|worktree| worktree.read(cx).abs_path())
.unwrap_or_else(|| paths::home_dir().as_path().into());
let (tx, mut rx) = watch::channel("Loading…".into());
let delegate = AgentServerDelegate::new(project.clone(), Some(tx));
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
let delegate = AgentServerDelegate::new(
project.clone(),
Some(status_tx),
Some(new_version_available_tx),
);
let connect_task = agent.connect(&root_dir, delegate, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
@@ -578,10 +629,23 @@ impl AcpThreadView {
.log_err();
});
cx.spawn(async move |this, cx| {
while let Ok(new_version) = new_version_available_rx.recv().await {
if let Some(new_version) = new_version {
this.update(cx, |this, cx| {
this.new_server_version_available = Some(new_version.into());
cx.notify();
})
.log_err();
}
}
})
.detach();
let loading_view = cx.new(|cx| {
let update_title_task = cx.spawn(async move |this, cx| {
loop {
let status = rx.recv().await?;
let status = status_rx.recv().await?;
this.update(cx, |this: &mut LoadingView, cx| {
this.title = status;
cx.notify();
@@ -617,17 +681,13 @@ impl AcpThreadView {
move |_, ev, window, cx| {
if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
&& &provider_id == updated_provider_id
&& LanguageModelRegistry::global(cx)
.read(cx)
.provider(&provider_id)
.map_or(false, |provider| provider.is_authenticated(cx))
{
this.update(cx, |this, cx| {
this.thread_state = Self::initial_state(
agent.clone(),
None,
this.workspace.clone(),
this.project.clone(),
window,
cx,
);
cx.notify();
this.reset(window, cx);
})
.ok();
}
@@ -827,6 +887,9 @@ impl AcpThreadView {
self.expanded_tool_calls.insert(tool_call_id.clone());
}
}
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
self.expanded_tool_calls.remove(tool_call_id);
}
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
if let Some(thread) = self.thread()
&& let Some(AgentThreadEntry::UserMessage(user_message)) =
@@ -899,6 +962,40 @@ impl AcpThreadView {
return;
}
let text = self.message_editor.read(cx).text(cx);
let text = text.trim();
if text == "/login" || text == "/logout" {
let ThreadState::Ready { thread, .. } = &self.thread_state else {
return;
};
let connection = thread.read(cx).connection().clone();
if !connection
.auth_methods()
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
return;
};
let this = cx.weak_entity();
let agent = self.agent.clone();
window.defer(cx, |window, cx| {
Self::handle_auth_required(
this,
AuthRequired {
description: None,
provider_id: None,
},
agent,
connection,
window,
cx,
);
});
cx.notify();
return;
}
let contents = self
.message_editor
.update(cx, |message_editor, cx| message_editor.contents(cx));
@@ -1189,6 +1286,14 @@ impl AcpThreadView {
cx,
);
}
AcpThreadEvent::Refusal => {
self.thread_retry_status.take();
self.thread_error = Some(ThreadError::Refusal);
let model_or_agent_name = self.get_current_model_name(cx);
let notification_message =
format!("{} refused to respond to this request", model_or_agent_name);
self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
}
AcpThreadEvent::Error => {
self.thread_retry_status.take();
self.notify_with_sound(
@@ -1220,6 +1325,30 @@ impl AcpThreadView {
.set(thread.read(cx).prompt_capabilities());
}
AcpThreadEvent::TokenUsageUpdated => {}
AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
let mut available_commands = available_commands.clone();
if thread
.read(cx)
.connection()
.auth_methods()
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
available_commands.push(acp::AvailableCommand {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
});
}
self.available_commands.replace(available_commands);
}
}
cx.notify();
}
@@ -1271,11 +1400,11 @@ impl AcpThreadView {
.read(cx)
.provider(&language_model::ANTHROPIC_PROVIDER_ID)
.unwrap();
if !provider.is_authenticated(cx) {
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, |window, cx| {
let this = cx.weak_entity();
let agent = self.agent.clone();
let connection = connection.clone();
window.defer(cx, move |window, cx| {
if !provider.is_authenticated(cx) {
Self::handle_auth_required(
this,
AuthRequired {
@@ -1287,9 +1416,21 @@ impl AcpThreadView {
window,
cx,
);
});
return;
}
} else {
this.update(cx, |this, cx| {
this.thread_state = Self::initial_state(
agent,
None,
this.workspace.clone(),
this.project.clone(),
window,
cx,
)
})
.ok();
}
});
return;
} else if method.0.as_ref() == "vertex-ai"
&& std::env::var("GOOGLE_API_KEY").is_err()
&& (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
@@ -1333,7 +1474,6 @@ impl AcpThreadView {
cx.notify();
self.auth_task =
Some(cx.spawn_in(window, {
let project = self.project.clone();
let agent = self.agent.clone();
async move |this, cx| {
let result = authenticate.await;
@@ -1362,14 +1502,7 @@ impl AcpThreadView {
}
this.handle_thread_error(err, cx);
} else {
this.thread_state = Self::initial_state(
agent,
None,
this.workspace.clone(),
project.clone(),
window,
cx,
)
this.reset(window, cx);
}
this.auth_task.take()
})
@@ -1391,7 +1524,7 @@ impl AcpThreadView {
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let delegate = AgentServerDelegate::new(project_entity.clone(), None);
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
let command = ClaudeCode::login_command(delegate, cx);
window.spawn(cx, async move |cx| {
@@ -2418,7 +2551,8 @@ impl AcpThreadView {
let output = terminal_data.output();
let command_finished = output.is_some();
let truncated_output = output.is_some_and(|output| output.was_content_truncated);
let truncated_output =
output.is_some_and(|output| output.original_content_len > output.content.len());
let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0);
let command_failed = command_finished
@@ -2506,48 +2640,20 @@ impl AcpThreadView {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
),
.with_rotate_animation(2)
)
})
.child(
Disclosure::new(
SharedString::from(format!(
"terminal-tool-disclosure-{}",
terminal.entity_id()
)),
is_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&header_group)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
if is_expanded {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
}
})),
)
.when(truncated_output, |header| {
let tooltip = if let Some(output) = output {
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
"Output exceeded terminal max lines and was \
truncated, the model received the first 16 KB."
.to_string()
format!("Output exceeded terminal max lines and was \
truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true))
} else {
format!(
"Output is {} long, and to avoid unexpected token usage, \
only 16 KB was sent back to the model.",
only {} was sent back to the agent.",
format_file_size(output.original_content_len as u64, true),
format_file_size(output.content.len() as u64, true)
)
}
} else {
@@ -2595,7 +2701,29 @@ impl AcpThreadView {
)))
}),
)
});
})
.child(
Disclosure::new(
SharedString::from(format!(
"terminal-tool-disclosure-{}",
terminal.entity_id()
)),
is_expanded,
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.visible_on_hover(&header_group)
.on_click(cx.listener({
let id = tool_call.id.clone();
move |this, _event, _window, _cx| {
if is_expanded {
this.expanded_tool_calls.remove(&id);
} else {
this.expanded_tool_calls.insert(id.clone());
}
}
})),
);
let terminal_view = self
.entry_view_state
@@ -2646,7 +2774,18 @@ impl AcpThreadView {
.bg(cx.theme().colors().editor_background)
.rounded_b_md()
.text_ui_sm(cx)
.children(terminal_view.clone()),
.h_full()
.children(terminal_view.map(|terminal_view| {
if terminal_view
.read(cx)
.content_mode(window, cx)
.is_scrollable()
{
div().h_72().child(terminal_view).into_any_element()
} else {
terminal_view.into_any_element()
}
})),
)
})
.into_any()
@@ -2872,6 +3011,8 @@ impl AcpThreadView {
let show_description =
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
let auth_methods = connection.auth_methods();
v_flex().flex_1().size_full().justify_end().child(
v_flex()
.p_2()
@@ -2902,21 +3043,23 @@ impl AcpThreadView {
.cloned()
.map(|view| div().w_full().child(view)),
)
.when(
show_description,
|el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}. Please choose one of the following options:",
self.agent.name()
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
},
)
.when(show_description, |el| {
el.child(
Label::new(format!(
"You are not currently authenticated with {}.{}",
self.agent.name(),
if auth_methods.len() > 1 {
" Please choose one of the following options:"
} else {
""
}
))
.size(LabelSize::Small)
.color(Color::Muted)
.mb_1()
.ml_5(),
)
})
.when_some(pending_auth_method, |el, _| {
el.child(
h_flex()
@@ -2928,21 +3071,12 @@ impl AcpThreadView {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
)
.into_any_element(),
.with_rotate_animation(2),
)
.child(Label::new("Authenticating…").size(LabelSize::Small)),
)
})
.when(!connection.auth_methods().is_empty(), |this| {
.when(!auth_methods.is_empty(), |this| {
this.child(
h_flex()
.justify_end()
@@ -2954,38 +3088,32 @@ impl AcpThreadView {
.pt_2()
.border_color(cx.theme().colors().border.opacity(0.8))
})
.children(
connection
.auth_methods()
.iter()
.enumerate()
.rev()
.map(|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
this.authenticate(method_id.clone(), window, cx)
})
this.authenticate(method_id.clone(), window, cx)
})
}),
),
})
},
)),
)
})
}),
)
}
@@ -3250,13 +3378,7 @@ impl AcpThreadView {
acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"running",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(delta)))
},
)
.with_rotate_animation(2)
.into_any_element(),
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
@@ -4614,9 +4736,9 @@ impl AcpThreadView {
)
}
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.entry_view_state.update(cx, |entry_view_state, cx| {
entry_view_state.settings_changed(cx);
entry_view_state.agent_font_size_changed(cx);
});
}
@@ -4683,6 +4805,7 @@ impl AcpThreadView {
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
let content = match self.thread_error.as_ref()? {
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
ThreadError::Refusal => self.render_refusal_error(cx),
ThreadError::AuthenticationRequired(error) => {
self.render_authentication_required_error(error.clone(), cx)
}
@@ -4698,6 +4821,75 @@ impl AcpThreadView {
Some(div().child(content))
}
fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context<Self>) -> Div {
v_flex().w_full().justify_end().child(
h_flex()
.p_2()
.pr_3()
.w_full()
.gap_1p5()
.border_t_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().element_background)
.child(
h_flex()
.flex_1()
.gap_1p5()
.child(
Icon::new(IconName::Download)
.color(Color::Accent)
.size(IconSize::Small),
)
.child(Label::new("New version available").size(LabelSize::Small)),
)
.child(
Button::new("update-button", format!("Update to v{}", version))
.label_size(LabelSize::Small)
.style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(cx.listener(|this, _, window, cx| {
this.reset(window, cx);
})),
),
)
}
fn get_current_model_name(&self, cx: &App) -> SharedString {
// For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
// For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
// This provides better clarity about what refused the request
if self
.agent
.clone()
.downcast::<agent2::NativeAgentServer>()
.is_some()
{
// Native agent - use the model name
self.model_selector
.as_ref()
.and_then(|selector| selector.read(cx).active_model_name(cx))
.unwrap_or_else(|| SharedString::from("The model"))
} else {
// ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
self.agent.name()
}
}
fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
let model_or_agent_name = self.get_current_model_name(cx);
let refusal_message = format!(
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
model_or_agent_name
);
Callout::new()
.severity(Severity::Error)
.title("Request Refused")
.icon(IconName::XCircle)
.description(refusal_message.clone())
.actions_slot(self.create_copy_button(&refusal_message))
.dismiss_action(self.dismiss_error_button(cx))
}
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
let can_resume = self
.thread()
@@ -4980,11 +5172,7 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
Icon::new(IconName::LoadCircle)
.size(size)
.color(Color::Accent)
.with_animation(
"load_context_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(3)
.into_any_element()
}
@@ -5075,6 +5263,12 @@ impl Render for AcpThreadView {
})
.children(self.render_thread_retry_status_callout(window, cx))
.children(self.render_thread_error(window, cx))
.when_some(
self.new_server_version_available.as_ref().filter(|_| {
!has_messages || !matches!(self.thread_state, ThreadState::Ready { .. })
}),
|this, version| this.child(self.render_new_version_callout(&version, cx)),
)
.children(
if let Some(usage_callout) = self.render_usage_callout(line_height, cx) {
Some(usage_callout.into_any_element())
@@ -5329,6 +5523,33 @@ pub(crate) mod tests {
);
}
#[gpui::test]
async fn test_refusal_handling(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) =
setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Do something harmful", window, cx);
});
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.send(window, cx);
});
cx.run_until_parked();
// Check that the refusal error is set
thread_view.read_with(cx, |thread_view, _cx| {
assert!(
matches!(thread_view.thread_error, Some(ThreadError::Refusal)),
"Expected refusal error to be set"
);
});
}
#[gpui::test]
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
@@ -5563,6 +5784,67 @@ pub(crate) mod tests {
}
}
/// Simulates a model which always returns a refusal response
#[derive(Clone)]
struct RefusalAgentConnection;
impl AgentConnection for RefusalAgentConnection {
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
Task::ready(Ok(cx.new(|cx| {
let action_log = cx.new(|_| ActionLog::new(project.clone()));
AcpThread::new(
"RefusalAgentConnection",
self,
project,
action_log,
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
}),
cx,
)
})))
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}
fn authenticate(
&self,
_method_id: acp::AuthMethodId,
_cx: &mut App,
) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(
&self,
_id: Option<acp_thread::UserMessageId>,
_params: acp::PromptRequest,
_cx: &mut App,
) -> Task<gpui::Result<acp::PromptResponse>> {
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
}))
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
unimplemented!()
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
pub(crate) fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);

View File

@@ -23,9 +23,8 @@ use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage,
pulsating_between,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle,
WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between,
};
use language::{Buffer, Language, LanguageRegistry};
use language_model::{
@@ -46,8 +45,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar,
ScrollbarState, TextSize, Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@@ -1002,8 +1001,22 @@ impl ActiveThread {
// Don't notify for intermediate tool use
}
Ok(StopReason::Refusal) => {
let model_name = self
.thread
.read(cx)
.configured_model()
.map(|configured| configured.model.name().0.to_string())
.unwrap_or_else(|| "The model".to_string());
let refusal_message = format!(
"{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
model_name
);
self.last_error = Some(ThreadError::Message {
header: SharedString::from("Request Refused"),
message: SharedString::from(refusal_message),
});
self.notify_with_sound(
"Language model refused to respond",
format!("{} refused to respond", model_name),
IconName::Warning,
window,
cx,
@@ -2647,15 +2660,7 @@ impl ActiveThread {
Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
)
.with_rotate_animation(2)
}),
),
)
@@ -2831,17 +2836,11 @@ impl ActiveThread {
}
ToolUseStatus::Pending
| ToolUseStatus::InputStillStreaming
| ToolUseStatus::Running => {
let icon = Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small);
icon.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
}
| ToolUseStatus::Running => Icon::new(IconName::ArrowCircle)
.color(Color::Accent)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
ToolUseStatus::Finished(_) => div().w_0().into_any_element(),
ToolUseStatus::Error(_) => {
let icon = Icon::new(IconName::Close)
@@ -2930,15 +2929,7 @@ impl ActiveThread {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
),
.with_rotate_animation(2),
)
.child(
Label::new("Running…")

View File

@@ -3,9 +3,9 @@ mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
use std::{ops::Range, sync::Arc, time::Duration};
use std::{ops::Range, sync::Arc};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
use agent_servers::{AgentServerCommand, AllAgentServersSettings};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -17,10 +17,10 @@ use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity,
EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation,
WeakEntity, percentage,
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, ScrollHandle, Subscription, Task, WeakEntity,
};
use itertools::Itertools as _;
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
@@ -32,8 +32,9 @@ use project::{
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*,
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
@@ -190,7 +191,7 @@ impl AgentConfiguration {
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected()
!workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(false);
@@ -331,6 +332,7 @@ impl AgentConfiguration {
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
@@ -669,10 +671,9 @@ impl AgentConfiguration {
Icon::new(IconName::LoadCircle)
.size(IconSize::XSmall)
.color(Color::Accent)
.with_animation(
SharedString::from(format!("{}-starting", context_server_id.0,)),
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
.with_keyed_rotate_animation(
SharedString::from(format!("{}-starting", context_server_id.0)),
3,
)
.into_any_element(),
"Server is starting.",
@@ -993,15 +994,16 @@ impl AgentConfiguration {
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
.custom
.iter()
.map(|(name, settings)| {
.filter(|(name, _)| *name != "gemini" && *name != "claude")
.sorted_by(|(l, _), (r, _)| l.cmp(r))
.map(|(name, command)| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone(),
command: settings.command.clone(),
command: command.clone(),
},
cx,
)
@@ -1022,6 +1024,7 @@ impl AgentConfiguration {
.gap_0p5()
.child(
h_flex()
.pr_1()
.w_full()
.gap_2()
.justify_between()
@@ -1052,7 +1055,7 @@ impl AgentConfiguration {
)
.child(
Label::new(
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
"All agents connected through the Agent Client Protocol.",
)
.color(Color::Muted),
),
@@ -1063,7 +1066,12 @@ impl AgentConfiguration {
ExternalAgent::Gemini,
cx,
))
// TODO add CC
.child(self.render_agent_server(
IconName::AiClaude,
"Claude Code",
ExternalAgent::ClaudeCode,
cx,
))
.children(user_defined_agents),
)
}
@@ -1093,26 +1101,24 @@ impl AgentConfiguration {
.child(Label::new(name.clone())),
)
.child(
h_flex().gap_1().child(
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
),
Button::new(
SharedString::from(format!("start_acp_thread-{name}")),
"Start New Thread",
)
.label_size(LabelSize::Small)
.icon(IconName::Thread)
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(move |_, window, cx| {
window.dispatch_action(
NewExternalAgentThread {
agent: Some(agent.clone()),
}
.boxed_clone(),
cx,
);
}),
)
}
}
@@ -1276,6 +1282,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
// FIXME test that this still works
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
@@ -1290,12 +1297,10 @@ async fn open_new_agent_servers_entry_in_settings_editor(
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
CustomAgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
);
}

View File

@@ -1,16 +1,14 @@
use std::{
path::PathBuf,
sync::{Arc, Mutex},
time::Duration,
};
use anyhow::{Context as _, Result};
use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
WeakEntity, percentage, prelude::*,
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
};
use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
@@ -24,7 +22,9 @@ use project::{
};
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
use ui::{
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
@@ -638,11 +638,7 @@ impl ConfigureContextServerModal {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.with_rotate_animation(2)
.into_any_element(),
)
.child(

View File

@@ -10,12 +10,12 @@ use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
multibuffer_context_lines,
scroll::Autoscroll,
};
use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
WeakEntity, Window, percentage, prelude::*,
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -28,9 +28,8 @@ use std::{
collections::hash_map::Entry,
ops::Range,
sync::Arc,
time::Duration,
};
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
use workspace::{
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
@@ -257,7 +256,7 @@ impl AgentDiffPane {
path_key.clone(),
buffer.clone(),
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(diff_handle, cx);
@@ -1083,11 +1082,7 @@ impl Render for AgentDiffToolbar {
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"load_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(3),
)
.into_any();
@@ -1522,7 +1517,10 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx);
}
}
AcpThreadEvent::Stopped | AcpThreadEvent::Error | AcpThreadEvent::LoadError(_) => {
AcpThreadEvent::Stopped
| AcpThreadEvent::Error
| AcpThreadEvent::LoadError(_)
| AcpThreadEvent::Refusal => {
self.update_reviewing_editors(workspace, window, cx);
}
AcpThreadEvent::TitleUpdated
@@ -1530,6 +1528,7 @@ impl AgentDiff {
| AcpThreadEvent::EntriesRemoved(_)
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::AvailableCommandsUpdated(_)
| AcpThreadEvent::Retry(_) => {}
}
}

View File

@@ -8,13 +8,14 @@ use acp_thread::AcpThread;
use agent_servers::AgentServerCommand;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::ReauthenticateAgent;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
use crate::agent_diff::AgentDiffThread;
use crate::ui::AcpOnboardingModal;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -207,6 +208,9 @@ pub fn init(cx: &mut App) {
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
AcpOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|workspace, _: &OpenClaudeCodeOnboardingModal, window, cx| {
ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
})
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
window.refresh();
@@ -284,6 +288,17 @@ impl AgentType {
}
}
impl From<ExternalAgent> for AgentType {
fn from(value: ExternalAgent) -> Self {
match value {
ExternalAgent::Gemini => Self::Gemini,
ExternalAgent::ClaudeCode => Self::ClaudeCode,
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
ExternalAgent::NativeAgent => Self::NativeAgent,
}
}
}
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
@@ -1049,6 +1064,11 @@ impl AgentPanel {
editor
});
if self.selected_agent != AgentType::TextThread {
self.selected_agent = AgentType::TextThread;
self.serialize(cx);
}
self.set_active_view(
ActiveView::prompt_editor(
context_editor.clone(),
@@ -1075,6 +1095,7 @@ impl AgentPanel {
let workspace = self.workspace.clone();
let project = self.project.clone();
let fs = self.fs.clone();
let is_not_local = !self.project.read(cx).is_local();
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
@@ -1106,17 +1127,21 @@ impl AgentPanel {
agent
}
None => {
cx.background_spawn(async move {
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
})
.await
.log_err()
.flatten()
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.unwrap_or_default()
.agent
if is_not_local {
ExternalAgent::NativeAgent
} else {
cx.background_spawn(async move {
KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
})
.await
.log_err()
.flatten()
.and_then(|value| {
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
})
.unwrap_or_default()
.agent
}
}
};
@@ -1140,6 +1165,12 @@ impl AgentPanel {
}
}
let selected_agent = ext_agent.into();
if this.selected_agent != selected_agent {
this.selected_agent = selected_agent;
this.serialize(cx);
}
let thread_view = cx.new(|cx| {
crate::acp::AcpThreadView::new(
server,
@@ -1235,6 +1266,12 @@ impl AgentPanel {
cx,
)
});
if self.selected_agent != AgentType::TextThread {
self.selected_agent = AgentType::TextThread;
self.serialize(cx);
}
self.set_active_view(
ActiveView::prompt_editor(
editor,
@@ -1860,11 +1897,6 @@ impl AgentPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.selected_agent != agent {
self.selected_agent = agent.clone();
self.serialize(cx);
}
match agent {
AgentType::Zed => {
window.dispatch_action(
@@ -1888,13 +1920,17 @@ impl AgentPanel {
AgentType::Gemini => {
self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
}
AgentType::ClaudeCode => self.external_thread(
Some(crate::ExternalAgent::ClaudeCode),
None,
None,
window,
cx,
),
AgentType::ClaudeCode => {
self.selected_agent = AgentType::ClaudeCode;
self.serialize(cx);
self.external_thread(
Some(crate::ExternalAgent::ClaudeCode),
None,
None,
window,
cx,
)
}
AgentType::Custom { name, command } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, command }),
None,
@@ -2500,6 +2536,9 @@ impl AgentPanel {
.with_handle(self.new_thread_menu_handle.clone())
.menu({
let workspace = self.workspace.clone();
let is_not_local = workspace
.update(cx, |workspace, cx| !workspace.project().read(cx).is_local())
.unwrap_or_default();
move |window, cx| {
telemetry::event!("New Thread Clicked");
@@ -2590,6 +2629,7 @@ impl AgentPanel {
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.disabled(is_not_local)
.handler({
let workspace = workspace.clone();
move |window, cx| {
@@ -2616,6 +2656,7 @@ impl AgentPanel {
menu.item(
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.disabled(is_not_local)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
@@ -2643,15 +2684,22 @@ impl AgentPanel {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, agent_settings) in &settings.custom {
for (agent_name, command) in
settings.iter().sorted_by(|(l, _), (r, _)| l.cmp(r))
{
if agent_name == "gemini" || agent_name == "claude" {
continue;
}
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
.icon_color(Color::Muted)
.disabled(is_not_local)
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let agent_settings = agent_settings.clone();
let command = command.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -2663,8 +2711,7 @@ impl AgentPanel {
AgentType::Custom {
name: agent_name
.clone(),
command: agent_settings
.command
command: command
.clone(),
},
window,
@@ -2926,6 +2973,20 @@ impl AgentPanel {
return false;
}
let user_store = self.user_store.read(cx);
if user_store
.plan()
.is_some_and(|plan| matches!(plan, Plan::ZedPro))
&& user_store
.subscription_period()
.and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
.is_some_and(|date| date < chrono::Utc::now())
{
OnboardingUpsell::set_dismissed(true, cx);
return false;
}
match &self.active_view {
ActiveView::History | ActiveView::Configuration => false,
ActiveView::ExternalAgentThread { thread_view, .. }
@@ -3494,6 +3555,11 @@ impl AgentPanel {
) -> AnyElement {
let message_with_header = format!("{}\n{}", header, message);
// Don't show Retry button for refusals
let is_refusal = header == "Request Refused";
let retry_button = self.render_retry_button(thread);
let copy_button = self.create_copy_button(message_with_header);
Callout::new()
.severity(Severity::Error)
.icon(IconName::XCircle)
@@ -3502,8 +3568,8 @@ impl AgentPanel {
.actions_slot(
h_flex()
.gap_0p5()
.child(self.render_retry_button(thread))
.child(self.create_copy_button(message_with_header)),
.when(!is_refusal, |this| this.child(retry_button))
.child(copy_button),
)
.dismiss_action(self.dismiss_error_button(thread, cx))
.into_any_element()

View File

@@ -1139,7 +1139,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1208,7 +1208,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;
@@ -1277,7 +1277,7 @@ mod tests {
);
while !new_text.is_empty() {
let max_len = cmp::min(new_text.len(), 10);
let len = rng.gen_range(1..=max_len);
let len = rng.random_range(1..=max_len);
let (chunk, suffix) = new_text.split_at(len);
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
new_text = suffix;

View File

@@ -13,7 +13,10 @@ use http_client::HttpClientWithUrl;
use itertools::Itertools;
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::{Completion, CompletionIntent, CompletionResponse, ProjectPath, Symbol, WorktreeId};
use project::{
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath,
Symbol, WorktreeId,
};
use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, OffsetRangeExt, ToPoint};
@@ -897,6 +900,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
Ok(vec![CompletionResponse {
completions,
display_options: CompletionDisplayOptions::default(),
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,

View File

@@ -144,7 +144,8 @@ impl InlineAssistant {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return;
};
let enabled = AgentSettings::get_global(cx).enabled;
let enabled = !DisableAiSettings::get_global(cx).disable_ai
&& AgentSettings::get_global(cx).enabled;
terminal_panel.update(cx, |terminal_panel, cx| {
terminal_panel.set_assistant_enabled(enabled, cx)
});

View File

@@ -93,8 +93,8 @@ impl<T: 'static> Render for PromptEditor<T> {
};
let bottom_padding = match &self.mode {
PromptEditorMode::Buffer { .. } => Pixels::from(0.),
PromptEditorMode::Terminal { .. } => Pixels::from(8.0),
PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
};
buttons.extend(self.render_buttons(window, cx));
@@ -334,7 +334,7 @@ impl<T: 'static> PromptEditor<T> {
EditorEvent::Edited { .. } => {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_ssh();
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
workspace
.client()
@@ -762,20 +762,22 @@ impl<T: 'static> PromptEditor<T> {
)
}
fn render_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(window.rem_size()) * 1.3;
fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
let colors = cx.theme().colors();
div()
.key_context("InlineAssistEditor")
.size_full()
.p_2()
.pl_1()
.bg(cx.theme().colors().editor_background)
.bg(colors.editor_background)
.child({
let settings = ThemeSettings::get_global(cx);
let font_size = settings.buffer_font_size(cx);
let line_height = font_size * 1.2;
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
color: colors.editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_size: font_size.into(),
@@ -786,7 +788,7 @@ impl<T: 'static> PromptEditor<T> {
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
background: colors.editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()

View File

@@ -7,7 +7,10 @@ use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity, Window};
use language::{Anchor, Buffer, ToPoint};
use parking_lot::Mutex;
use project::{CompletionIntent, CompletionSource, lsp_store::CompletionDocumentation};
use project::{
CompletionDisplayOptions, CompletionIntent, CompletionSource,
lsp_store::CompletionDocumentation,
};
use rope::Point;
use std::{
ops::Range,
@@ -133,6 +136,7 @@ impl SlashCommandCompletionProvider {
vec![project::CompletionResponse {
completions,
display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}]
})
@@ -237,6 +241,7 @@ impl SlashCommandCompletionProvider {
Ok(vec![project::CompletionResponse {
completions,
display_options: CompletionDisplayOptions::default(),
// TODO: Could have slash commands indicate whether their completions are incomplete.
is_incomplete: true,
}])
@@ -244,6 +249,7 @@ impl SlashCommandCompletionProvider {
} else {
Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
display_options: CompletionDisplayOptions::default(),
is_incomplete: true,
}]))
}
@@ -305,6 +311,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
else {
return Task::ready(Ok(vec![project::CompletionResponse {
completions: Vec::new(),
display_options: CompletionDisplayOptions::default(),
is_incomplete: false,
}]));
};

View File

@@ -2,10 +2,11 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
/// Settings for slash commands.
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
#[settings_key(key = "slash_commands")]
pub struct SlashCommandSettings {
/// Settings for the `/cargo-workspace` slash command.
#[serde(default)]
@@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
}
impl Settings for SlashCommandSettings {
const KEY: Option<&'static str> = Some("slash_commands");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {

View File

@@ -25,8 +25,8 @@ use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
div, img, percentage, point, prelude::*, pulsating_between, size,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
prelude::*, pulsating_between, size,
};
use language::{
BufferSnapshot, LspAdapterDelegate, ToOffset,
@@ -53,8 +53,8 @@ use std::{
};
use text::SelectionGoal;
use ui::{
ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip,
prelude::*,
ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle,
TintColor, Tooltip, prelude::*,
};
use util::{ResultExt, maybe};
use workspace::{
@@ -1061,15 +1061,7 @@ impl TextThreadEditor {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(
percentage(delta),
))
},
)
.with_rotate_animation(2)
.into_any_element(),
);
note = Some(Self::esc_kbd(cx).into_any_element());
@@ -2790,11 +2782,7 @@ fn invoked_slash_command_fold_placeholder(
.child(Label::new(format!("/{}", command.name)))
.map(|parent| match &command.status {
InvokedSlashCommandStatus::Running(_) => {
parent.child(Icon::new(IconName::ArrowCircle).with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(4)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
))
parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4))
}
InvokedSlashCommandStatus::Error(message) => parent.child(
Label::new(format!("error: {message}"))

View File

@@ -1,6 +1,7 @@
mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
mod context_pill;
mod end_trial_upsell;
mod onboarding_modal;
@@ -10,6 +11,7 @@ mod unavailable_editing_tooltip;
pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
pub use onboarding_modal::*;

View File

@@ -141,20 +141,12 @@ impl Render for AcpOnboardingModal {
.bg(gpui::black().opacity(0.15)),
)
.child(
h_flex()
.gap_4()
.child(
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
)
.child(
Vector::new(
VectorName::AcpLogoSerif,
rems_from_px(111.),
rems_from_px(41.),
)
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
),
Vector::new(
VectorName::AcpLogoSerif,
rems_from_px(257.),
rems_from_px(47.),
)
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
)
.child(
v_flex()

View File

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

View File

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

View File

@@ -1,22 +1,19 @@
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use client::{Client, UserStore, zed_urls};
use cloud_llm_client::Plan;
use gpui::{
Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation,
Window, percentage,
};
use ui::{Divider, Vector, VectorName, prelude::*};
use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window};
use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*};
use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions};
#[derive(IntoElement, RegisterComponent)]
pub struct AiUpsellCard {
pub sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub account_too_young: bool,
pub user_plan: Option<Plan>,
pub tab_index: Option<isize>,
sign_in_status: SignInStatus,
sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
account_too_young: bool,
user_plan: Option<Plan>,
tab_index: Option<isize>,
}
impl AiUpsellCard {
@@ -43,6 +40,11 @@ impl AiUpsellCard {
tab_index: None,
}
}
pub fn tab_index(mut self, tab_index: Option<isize>) -> Self {
self.tab_index = tab_index;
self
}
}
impl RenderOnce for AiUpsellCard {
@@ -84,10 +86,16 @@ impl RenderOnce for AiUpsellCard {
)
.child(plan_definitions.free_plan());
let grid_bg = h_flex().absolute().inset_0().w_full().h(px(240.)).child(
Vector::new(VectorName::Grid, rems_from_px(500.), rems_from_px(240.))
.color(Color::Custom(cx.theme().colors().border.opacity(0.05))),
);
let grid_bg = h_flex()
.absolute()
.inset_0()
.w_full()
.h(px(240.))
.bg(gpui::pattern_slash(
cx.theme().colors().border.opacity(0.1),
2.,
25.,
));
let gradient_bg = div()
.absolute()
@@ -142,11 +150,7 @@ impl RenderOnce for AiUpsellCard {
rems_from_px(72.),
)
.color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3)))
.with_animation(
"loading_stamp",
Animation::new(Duration::from_secs(10)).repeat(),
|this, delta| this.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(10),
);
let pro_trial_stamp = div()

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 ui::prelude::*;
@@ -35,6 +36,8 @@ impl EditPredictionOnboarding {
impl Render for EditPredictionOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_free_plan = self.user_store.read(cx).plan() == Some(Plan::ZedFree);
let github_copilot = v_flex()
.gap_1()
.child(Label::new(if self.copilot_is_configured {
@@ -67,7 +70,8 @@ impl Render for EditPredictionOnboarding {
self.continue_with_zed_ai.clone(),
cx,
))
.child(ui::Divider::horizontal())
.child(github_copilot)
.when(is_free_plan, |this| {
this.child(ui::Divider::horizontal()).child(github_copilot)
})
}
}

View File

@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
impl RenderOnce for YoungAccountBanner {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev.";
const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, GitHub accounts created fewer than 30 days ago are not eligible for free plan usage or Pro plan free trial. To request an exception, reach out to billing-support@zed.dev.";
let label = div()
.w_full()

View File

@@ -363,17 +363,15 @@ pub async fn complete(
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
let beta_headers = Model::from_id(&request.model)
.map(|model| model.beta_headers())
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
@@ -409,8 +407,9 @@ pub async fn stream_completion(
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request)
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
.await
.map(|output| output.0)
}
@@ -506,6 +505,7 @@ pub async fn stream_completion_with_rate_limit_info(
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
@@ -518,15 +518,13 @@ pub async fn stream_completion_with_rate_limit_info(
stream: true,
};
let uri = format!("{api_url}/v1/messages");
let beta_headers = Model::from_id(&request.base.model)
.map(|model| model.beta_headers())
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;

View File

@@ -50,8 +50,9 @@ text.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
workspace-hack.workspace = true
zed_env_vars.workspace = true
[dev-dependencies]
indoc.workspace = true

View File

@@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
let mut contexts = Vec::new();
let num_peers = rng.gen_range(min_peers..=max_peers);
let num_peers = rng.random_range(min_peers..=max_peers);
let context_id = ContextId::new();
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
for i in 0..num_peers {
@@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|| !network.lock().is_idle()
|| network.lock().contains_disconnected_peers()
{
let context_index = rng.gen_range(0..contexts.len());
let context_index = rng.random_range(0..contexts.len());
let context = &contexts[context_index];
match rng.gen_range(0..100) {
match rng.random_range(0..100) {
0..=29 if mutation_count > 0 => {
log::info!("Context {}: edit buffer", context_index);
context.update(cx, |context, cx| {
@@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
merge_same_roles: true,
})];
let num_sections = rng.gen_range(0..=3);
let num_sections = rng.random_range(0..=3);
let mut section_start = 0;
for _ in 0..num_sections {
let mut section_end = rng.gen_range(section_start..=output_text.len());
let mut section_end = rng.random_range(section_start..=output_text.len());
while !output_text.is_char_boundary(section_end) {
section_end += 1;
}
@@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
75..=84 if mutation_count > 0 => {
context.update(cx, |context, cx| {
if let Some(message) = context.messages(cx).choose(&mut rng) {
let new_status = match rng.gen_range(0..3) {
let new_status = match rng.random_range(0..3) {
0 => MessageStatus::Done,
1 => MessageStatus::Pending,
_ => MessageStatus::Error(SharedString::from("Random error")),
@@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
network.lock().broadcast(replica_id, ops_to_send);
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
} else if rng.gen_bool(0.1) && replica_id != 0 {
} else if rng.random_bool(0.1) && replica_id != 0 {
log::info!("Context {}: disconnecting", context_index);
network.lock().disconnect_peer(replica_id);
} else if network.lock().has_unreceived(replica_id) {

View File

@@ -24,6 +24,7 @@ use rpc::AnyProtoClient;
use std::sync::LazyLock;
use std::{cmp::Reverse, ffi::OsStr, mem, path::Path, sync::Arc, time::Duration};
use util::{ResultExt, TryFutureExt};
use zed_env_vars::ZED_STATELESS;
pub(crate) fn init(client: &AnyProtoClient) {
client.add_entity_message_handler(ContextStore::handle_advertise_contexts);
@@ -788,8 +789,6 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone();
cx.spawn(async move |this, cx| {
pub static ZED_STATELESS: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_STATELESS").is_ok_and(|v| !v.is_empty()));
if *ZED_STATELESS {
return Ok(());
}

View File

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

View File

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

View File

@@ -1315,17 +1315,17 @@ mod tests {
#[gpui::test(iterations = 100)]
async fn test_random_indents(mut rng: StdRng) {
let len = rng.gen_range(1..=100);
let len = rng.random_range(1..=100);
let new_text = util::RandomCharIter::new(&mut rng)
.with_simple_text()
.take(len)
.collect::<String>();
let new_text = new_text
.split('\n')
.map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
.map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line))
.collect::<Vec<_>>()
.join("\n");
let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize);
let chunks = to_random_chunks(&mut rng, &new_text);
let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
@@ -1357,7 +1357,7 @@ mod tests {
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -204,7 +204,7 @@ mod tests {
}
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -996,7 +996,7 @@ mod tests {
}
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -1399,7 +1399,7 @@ fn eval(
}
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy());
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
let output = cx.executor().block_test(async {
let test = EditAgentTest::new(&mut cx).await;
@@ -1707,7 +1707,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
};
if let Some(retry_after) = retry_delay {
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0));
eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}");
Timer::after(retry_after + jitter).await;
} else {

View File

@@ -771,7 +771,7 @@ mod tests {
}
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
chunk_indices.sort();
chunk_indices.push(input.len());

View File

@@ -11,11 +11,13 @@ use assistant_tool::{
AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use editor::{
Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px,
TextStyleRefinement, WeakEntity, pulsating_between, px,
};
use indoc::formatdoc;
use language::{
@@ -42,7 +44,7 @@ use std::{
time::Duration,
};
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::ResultExt;
use workspace::Workspace;
@@ -474,7 +476,7 @@ impl Tool for EditFileTool {
PathKey::for_buffer(&buffer, cx),
buffer,
diff_hunk_ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff, cx);
@@ -703,7 +705,7 @@ impl EditFileToolCard {
PathKey::for_buffer(buffer, cx),
buffer.clone(),
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
let end = multibuffer.len(cx);
@@ -791,7 +793,7 @@ impl EditFileToolCard {
path_key,
buffer,
ranges,
editor::DEFAULT_MULTIBUFFER_CONTEXT,
multibuffer_context_lines(cx),
cx,
);
multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -937,11 +939,7 @@ impl ToolCard for EditFileToolCard {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(2),
)
})
.when_some(error_message, |header, error_message| {

View File

@@ -8,14 +8,14 @@ use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task,
TextStyleRefinement, Transformation, WeakEntity, Window, percentage,
AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement,
WeakEntity, Window,
};
use language::LineEnding;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use project::{Project, terminals::TerminalKind};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -28,7 +28,7 @@ use std::{
};
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, prelude::*};
use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*};
use util::{
ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
time::duration_alt_display,
@@ -213,17 +213,16 @@ impl Tool for TerminalTool {
async move |cx| {
let program = program.await;
let env = env.await;
project
.update(cx, |project, cx| {
project.create_terminal(
TerminalKind::Task(task::SpawnInTerminal {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(program),
args,
cwd,
env,
..Default::default()
}),
},
cx,
)
})?
@@ -523,11 +522,7 @@ impl ToolCard for TerminalToolCard {
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Info)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
.with_rotate_animation(2),
)
})
.when(tool_failed || command_failed, |header| {

View File

@@ -15,9 +15,10 @@ doctest = false
[dependencies]
anyhow.workspace = true
collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
settings.workspace = true
schemars.workspace = true
serde.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
util.workspace = true
workspace-hack.workspace = true

View File

@@ -1,54 +0,0 @@
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};
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
pub struct SoundRegistry {
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
assets: Box<dyn AssetSource>,
}
struct GlobalSoundRegistry(Arc<SoundRegistry>);
impl Global for GlobalSoundRegistry {}
impl SoundRegistry {
pub fn new(source: impl AssetSource) -> Arc<Self> {
Arc::new(Self {
cache: Default::default(),
assets: Box::new(source),
})
}
pub fn global(cx: &App) -> Arc<Self> {
cx.global::<GlobalSoundRegistry>().0.clone()
}
pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) {
cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source)));
}
pub fn get(&self, name: &str) -> Result<impl Source<Item = f32> + use<>> {
if let Some(wav) = self.cache.lock().get(name) {
return Ok(wav.clone());
}
let path = format!("sounds/{}.wav", name);
let bytes = self
.assets
.load(&path)?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.buffered();
self.cache.lock().insert(name.to_string(), source.clone());
Ok(source)
}
}

View File

@@ -1,16 +1,19 @@
use assets::SoundRegistry;
use derive_more::{Deref, DerefMut};
use gpui::{App, AssetSource, BorrowAppContext, Global};
use rodio::{OutputStream, OutputStreamBuilder};
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use gpui::{App, BorrowAppContext, Global};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
use settings::Settings;
use std::io::Cursor;
use util::ResultExt;
mod assets;
mod audio_settings;
pub use audio_settings::AudioSettings;
pub fn init(source: impl AssetSource, cx: &mut App) {
SoundRegistry::set_global(source, cx);
cx.set_global(GlobalAudio(Audio::new()));
pub fn init(cx: &mut App) {
AudioSettings::register(cx);
}
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
pub enum Sound {
Joined,
Leave,
@@ -38,18 +41,12 @@ impl Sound {
#[derive(Default)]
pub struct Audio {
output_handle: Option<OutputStream>,
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
}
#[derive(Deref, DerefMut)]
struct GlobalAudio(Audio);
impl Global for GlobalAudio {}
impl Global for Audio {}
impl Audio {
pub fn new() -> Self {
Self::default()
}
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
if self.output_handle.is_none() {
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
@@ -58,26 +55,51 @@ impl Audio {
self.output_handle.as_ref()
}
pub fn play_sound(sound: Sound, cx: &mut App) {
if !cx.has_global::<GlobalAudio>() {
return;
}
pub fn play_source(
source: impl rodio::Source + Send + 'static,
cx: &mut App,
) -> anyhow::Result<()> {
cx.update_default_global(|this: &mut Self, _cx| {
let output_handle = this
.ensure_output_exists()
.ok_or_else(|| anyhow!("Could not open audio output"))?;
output_handle.mixer().add(source);
Ok(())
})
}
cx.update_global::<GlobalAudio, _>(|this, cx| {
pub fn play_sound(sound: Sound, cx: &mut App) {
cx.update_default_global(|this: &mut Self, cx| {
let source = this.sound_source(sound, cx).log_err()?;
let output_handle = this.ensure_output_exists()?;
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
output_handle.mixer().add(source);
Some(())
});
}
pub fn end_call(cx: &mut App) {
if !cx.has_global::<GlobalAudio>() {
return;
}
cx.update_global::<GlobalAudio, _>(|this, _| {
cx.update_default_global(|this: &mut Self, _cx| {
this.output_handle.take();
});
}
fn sound_source(&mut self, sound: Sound, cx: &App) -> Result<impl Source + use<>> {
if let Some(wav) = self.source_cache.get(&sound) {
return Ok(wav.clone());
}
let path = format!("sounds/{}.wav", sound.file());
let bytes = cx
.asset_source()
.load(&path)?
.map(anyhow::Ok)
.with_context(|| format!("No asset available for path {path}"))??
.into_owned();
let cursor = Cursor::new(bytes);
let source = Decoder::new(cursor)?.buffered();
self.source_cache.insert(sound, source.clone());
Ok(source)
}
}

View File

@@ -0,0 +1,32 @@
use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct AudioSettings {
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool, // default is false
}
/// Configuration of audio in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[serde(default)]
#[settings_key(key = "audio")]
pub struct AudioSettingsContent {
/// Whether to use the experimental audio system
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool,
}
impl Settings for AudioSettings {
type FileContent = AudioSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
sources.json_merge()
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View File

@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
use smol::{fs, io::AsyncReadExt};
use smol::{fs::File, process::Command};
use std::{
@@ -118,14 +118,13 @@ struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
#[serde(transparent)]
#[settings_key(key = "auto_update")]
struct AutoUpdateSettingContent(bool);
impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<AutoUpdateSettingContent>;
type FileContent = AutoUpdateSettingContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let auto_update = [
@@ -135,17 +134,19 @@ impl Settings for AutoUpdateSetting {
sources.user,
]
.into_iter()
.find_map(|value| value.copied().flatten())
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
.find_map(|value| value.copied())
.unwrap_or(*sources.default);
Ok(Self(auto_update.0))
}
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.enum_setting("update.mode", current, |s| match s {
let mut cur = &mut Some(*current);
vscode.enum_setting("update.mode", &mut cur, |s| match s {
"none" | "manual" => Some(AutoUpdateSettingContent(false)),
_ => Some(AutoUpdateSettingContent(true)),
});
*current = cur.unwrap();
}
}

View File

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

View File

@@ -2044,10 +2044,10 @@ mod tests {
#[gpui::test(iterations = 100)]
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
fn gen_line(rng: &mut StdRng) -> String {
if rng.gen_bool(0.2) {
if rng.random_bool(0.2) {
"\n".to_owned()
} else {
let c = rng.gen_range('A'..='Z');
let c = rng.random_range('A'..='Z');
format!("{c}{c}{c}\n")
}
}
@@ -2066,7 +2066,7 @@ mod tests {
old_lines.into_iter()
};
let mut result = String::new();
let unchanged_count = rng.gen_range(0..=old_lines.len());
let unchanged_count = rng.random_range(0..=old_lines.len());
result +=
&old_lines
.by_ref()
@@ -2076,14 +2076,14 @@ mod tests {
s
});
while old_lines.len() > 0 {
let deleted_count = rng.gen_range(0..=old_lines.len());
let deleted_count = rng.random_range(0..=old_lines.len());
let _advance = old_lines
.by_ref()
.take(deleted_count)
.map(|line| line.len() + 1)
.sum::<usize>();
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
let added_count = rng.gen_range(minimum_added..=5);
let added_count = rng.random_range(minimum_added..=5);
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
result += &addition;
@@ -2092,7 +2092,8 @@ mod tests {
if blank_lines == old_lines.len() {
break;
};
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
let unchanged_count =
rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
result += &old_lines.by_ref().take(unchanged_count).fold(
String::new(),
|mut s, line| {
@@ -2149,7 +2150,7 @@ mod tests {
)
});
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
let mut index_text = if rng.r#gen() {
let mut index_text = if rng.random() {
Rope::from(head_text.as_str())
} else {
working_copy.as_rope().clone()
@@ -2165,7 +2166,7 @@ mod tests {
}
for _ in 0..operations {
let i = rng.gen_range(0..hunks.len());
let i = rng.random_range(0..hunks.len());
let hunk = &mut hunks[i];
let hunk_to_change = hunk.clone();
let stage = match hunk.secondary_status {

View File

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

View File

@@ -2,7 +2,7 @@ use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Deserialize, Debug)]
pub struct CallSettings {
@@ -11,7 +11,8 @@ pub struct CallSettings {
}
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "calls")]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
}
impl Settings for CallSettings {
const KEY: Option<&'static str> = Some("calls");
type FileContent = CallSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

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