Compare commits

..

386 Commits

Author SHA1 Message Date
Michael Benfield
a7cf3c306a in progress 2025-11-19 17:03:48 -08:00
Andrew Farkas
829be71061 Fix invalid Unicode in terms & conditions (#42906)
Closes #40210

Previously attempted in #40423 and #42756. Third time's the charm?

Release Notes:

- Fixed encoding error in terms & conditions displayed when installing
2025-11-19 13:00:35 -05:00
Finn Evers
2a2f5a9c7a Add callable workflow for extension repositories (#43082)
This starts the work on a workflow that can be invoked in extension CI
to test changes on extension repositories.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-19 17:47:34 +00:00
Kirill Bulatov
97b429953e gpui: Do not render ligatures between different styled text runs (#43080)
An attempt to re-land https://github.com/zed-industries/zed/pull/41043
Part of https://github.com/zed-industries/zed/issues/5259 (as `>>>`
forms a ligature that we need to break into differently colored tokens)

Before:

<img width="301" height="86" alt="image"
src="https://github.com/user-attachments/assets/e710391a-b8ad-4343-8344-c86fc5cb86b6"
/>

and


https://github.com/user-attachments/assets/ae77ba64-ca50-4b5d-9ee4-a7d46fcaeb34


After:
<img width="1254" height="302" alt="image"
src="https://github.com/user-attachments/assets/7fd5dba5-d798-4153-acf2-e38a1cb712ae"
/>


When certain combination of characters forms a ligature, it takes the
color of the first character.
Even though the runs are split already by color and other properties,
the underlying font system merges the runs together.

Attempts to modify color and other, unrelated to font size, parameters,
did not help on macOS, hence a somewhat odd approach was taken: runs get
interleaved font sizes: normal and "normal + a tiny bit more".
This is the only option that helped splitting the ligatures, and seems
to render fine.

Release Notes:

- Fixed ligatures forming between different text kinds

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-19 19:37:22 +02:00
John Tur
404ee53812 Fix Windows bundling (#43083)
The updated package from
https://github.com/zed-industries/zed/pull/43066 changed the paths of
these files in the nupkg.

Release Notes:

- N/A
2025-11-19 12:25:03 -05:00
localcc
17c30565fc Fix extension auto-install on first setup (#43078)
Release Notes:

- N/A
2025-11-19 16:41:39 +00:00
Lena
f05eef58c4 Stop the buggy stalebot for now (#43076)
Delay the stalebot runs until the end of the year since it's currently
broken and leaves unhelpful comments on all the issues, including feature requests. Bad
bot. Allegedly this bug will soon be gone
https://github.com/actions/stale/issues/1302 but it's too much work
protecting issues from the bot until then.

Release Notes:

- N/A
2025-11-19 17:09:28 +01:00
Joseph T. Lyons
52716bacef Bump Zed to v0.215 (#43075)
Release Notes:

- N/A
2025-11-19 16:04:35 +00:00
Smit Barmase
79be5cbfe2 editor: Fix prepaint recursion when updating stale sizes (#42896)
The bug is in the `place_near` logic, specifically the
`!row_block_types.contains_key(&(row - 1))` check. The problem isn’t
really that condition itself, but it’s that it relies on
`row_block_types`, which does not take into account that upon block
resizes, subsequent block start row moves up/down. Since `place_near`
depends on this incorrect map, it ends up causing incorrect resize syncs
to the block map, which then triggers more bad recursive calls. The
reason it worked till now in most of the cases is that recursive resizes
eventually lead to stabilizing it.

Before `place_near`, we never touched `row_block_types` during the first
prepaint pass because we knew it was based on outdated heights. Once all
heights are finalized, using it is fine.

The fix is to make sure `row_block_types` is accurate from the very
first prepaint pass by keeping an offset whenever a block shrinks or
expands. Now ideally it should take only one subsequent prepaint. But
due to shrinking, new custom/diagnostics blocks might come into the view
from below, which needs further prepaint calls for resolving. Right now,
tests pass after 2 subsequent prepaint calls. Just to be safe, we have
set it to 5.

<img width="500" alt="image"
src="https://github.com/user-attachments/assets/da3d32ff-5972-46d9-8597-b438e162552b"
/>

Release Notes:

- Fix issue where sometimes Zed used to experience freeze while working
with inline diagnostics.
2025-11-19 21:02:31 +05:30
Jakub Konka
a42676b6bb git: Put pending ops container out of snapshot (#43061)
This also fixes staging checkbox flickering.

Release Notes:

- Fixed staging checkbox flickering sporadically in the Git panel.
2025-11-19 14:56:10 +00:00
Ben Kunkle
39f8aefa8c zeta2: Improve context retrieval (#43014)
Closes #ISSUE

Release Notes:

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

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Max <max@zed.dev>
2025-11-19 14:44:58 +00:00
Lukas Wirth
1c1dfba7e3 windows: Bundle freshers conpty.dll builds (#43066)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-19 15:11:37 +01:00
Finn Evers
2c4fd24d37 gpui: Restore last window close behavior on macOS (#43058)
Follow-up to https://github.com/zed-industries/zed/pull/42391

Release Notes:

- Fixed an issue where Zed did not respect the `on_last_window_closed`
setting on macOS
2025-11-19 13:22:29 +00:00
Lukas Wirth
3125e78904 windows: Bundle new conpty.dll/OpenConsole.exe and use it for local builds on x86_64 (#43059)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-19 13:12:57 +00:00
Vasyl Protsiv
74d61aad7f util: Fix zip extraction (#42714)
I was trying to use Zed for Rust debugging on windows, but was getting
this warning in debugger console: "Could not initialize Python
interpreter - some features will be unavailable (e.g. debug
visualizers)."
As the warning suggests this led to bad debugging experience where the
variables were not visualized properly in the "Variables" panel.

After some investigation I found that the problem is that Zed silently
failed to extract all files from the debug adapter package
(https://github.com/vadimcn/codelldb/releases/download/v1.11.8/codelldb-win32-x64.vsix).
Particularly `python-lldb` folder was missing, which caused the warning.
The error occurred here:

cf7c64d77f/crates/util/src/archive.rs (L47)
And then gets ignored here:

cf7c64d77f/crates/dap/src/adapters.rs (L323-L326)
The simple fix is to update `async_zip` crate to version 0.0.18 where
this issue appears to be fixed. I also added logging instead of silently
ignoring the error, as I believe that would have helped to catch it
earlier.

To reproduce the original issue you can try to follow these steps:
0. (Optional) Remove/rename old codelldb adapter at
`%localappdata%\Zed\debug_adapters\CodeLLDB`. Restart Zed.
1. Create a simple Rust project. Make sure you use gnu toolchain (target
`x86_64-pc-windows-gnu`)
```rust
fn world() -> String {
    "world".into()
}

fn main() {
    let w = world();
    println!("hello {}", w);
}
```

2. Put a breakpoint on line 7 (`println`)
3. In the command palette choose "debugger: start" and then select "run
*crate name*"

Screenshot before the fix:

<img width="893" height="411" alt="image"
src="https://github.com/user-attachments/assets/78097690-b55e-4989-bfa4-20452560f9fc"
/>


<details>
<summary>Console before the fix</summary>

```
Checking latest version of CodeLLDB...
Downloading from https://github.com/vadimcn/codelldb/releases/download/v1.11.8/codelldb-win32-x64.vsix...
Download complete
Could not initialize Python interpreter - some features will be unavailable (e.g. debug visualizers).
Console is in 'commands' mode, prefix expressions with '?'.
warning: (x86_64) D:\repro\target\x86_64-pc-windows-gnu\debug\repro.exe unable to locate separate debug file (dwo, dwp). Debugging will be degraded.
Launching: D:\repro\target\x86_64-pc-windows-gnu\debug\repro.exe
Launched process 13836 from 'D:\repro\target\x86_64-pc-windows-gnu\debug\repro.exe'
error: repro.exe [0x0000000000002074]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x000000000000001a) attribute, but range extraction failed (invalid range list offset 0x1a), please file a bug and attach the file at the start of this error message
error: repro.exe [0x000000000000208c]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000025) attribute, but range extraction failed (invalid range list offset 0x25), please file a bug and attach the file at the start of this error message
error: repro.exe [0x00000000000020af]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000030) attribute, but range extraction failed (invalid range list offset 0x30), please file a bug and attach the file at the start of this error message
error: repro.exe [0x00000000000020c4]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x000000000000003b) attribute, but range extraction failed (invalid range list offset 0x3b), please file a bug and attach the file at the start of this error message
error: repro.exe [0x00000000000020fc]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000046) attribute, but range extraction failed (invalid range list offset 0x46), please file a bug and attach the file at the start of this error message
error: repro.exe [0x0000000000002130]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000046) attribute, but range extraction failed (invalid range list offset 0x46), please file a bug and attach the file at the start of this error message
> ? w
< {...}
```
</details>

Screenshot after the fix:
<img width="634" height="295" alt="image"
src="https://github.com/user-attachments/assets/67e36a64-97d2-406c-9216-7ac5b01f4101"
/>

<details>
<summary>Console after the fix</summary>

```
Checking latest version of CodeLLDB...
Downloading from https://github.com/vadimcn/codelldb/releases/download/v1.11.8/codelldb-win32-x64.vsix...
Download complete
Console is in 'commands' mode, prefix expressions with '?'.
Loading Rust formatters from C:\Users\Vasyl\.rustup\toolchains\1.91.1-x86_64-pc-windows-msvc\lib/rustlib/etc
warning: (x86_64) D:\repro\target\x86_64-pc-windows-gnu\debug\repro.exe unable to locate separate debug file (dwo, dwp). Debugging will be degraded.
Launching: D:\repro\target\x86_64-pc-windows-gnu\debug\repro.exe
Launched process 10364 from 'D:\repro\target\x86_64-pc-windows-gnu\debug\repro.exe'
error: repro.exe [0x0000000000002074]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x000000000000001a) attribute, but range extraction failed (invalid range list offset 0x1a), please file a bug and attach the file at the start of this error message
error: repro.exe [0x000000000000208c]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000025) attribute, but range extraction failed (invalid range list offset 0x25), please file a bug and attach the file at the start of this error message
error: repro.exe [0x00000000000020af]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000030) attribute, but range extraction failed (invalid range list offset 0x30), please file a bug and attach the file at the start of this error message
error: repro.exe [0x00000000000020c4]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x000000000000003b) attribute, but range extraction failed (invalid range list offset 0x3b), please file a bug and attach the file at the start of this error message
error: repro.exe [0x00000000000020fc]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000046) attribute, but range extraction failed (invalid range list offset 0x46), please file a bug and attach the file at the start of this error message
error: repro.exe [0x0000000000002130]: DIE has DW_AT_ranges(DW_FORM_sec_offset 0x0000000000000046) attribute, but range extraction failed (invalid range list offset 0x46), please file a bug and attach the file at the start of this error message
> ? w
< "world"
```
</details>

This fixes #33753

Release Notes:

- util: Fixed archive::extract_zip failing to extract some archives
2025-11-19 12:34:41 +01:00
Piotr Osiewicz
40dd4e2270 zeta: Add stats about context lines from patch that were retrieved during context retrieval (#43053)
A.K.A: Eval: Expect lines necessary to uniquely target every change in
"Expected Patch" to be included as context

Release Notes:

- N/A
2025-11-19 11:25:53 +00:00
xdBronch
9feb260216 lsp: Support deprecated completion item tag and advertise capability (#43000)
Release Notes:

- N/A
2025-11-19 12:19:58 +01:00
Lukas Wirth
5ccbe945a6 util: Check whether discovered powershell is actually executable (#43044)
Closes https://github.com/zed-industries/zed/issues/42944

The powershell we discovered might be in a directory with higher
permission requirements which will cause us to fail using it.

Release Notes:

- Fixed powershell discovery disregarding admin requirements
2025-11-19 09:26:49 +00:00
Bennet Bo Fenner
a910c594d6 agent_ui: Add mode_id to telemetry (#43045)
Release Notes:

- N/A
2025-11-19 08:49:56 +00:00
Mikayla Maki
19d2532cf8 Update google_ai.rs (#43034)
Release Notes:

- N/A
2025-11-19 05:41:24 +00:00
Cole Miller
785b81aa3a Revert "Fix track file renames in git panel (#42352)" (#43030)
This reverts commit b0a7defd09.

It looks like this doesn't interact correctly with the project diff or
with staging, let's revert and reland with bugs fixed.

Release Notes:

- N/A
2025-11-19 03:56:45 +00:00
Ben Heimberg
24c1617e74 git_ui: Dismiss pickers only on active window (#41320)
Small QOL improvement for branch picker to only dismiss when focus lost
in active window.
This can benefit those who need to switch windows mid branch creation to
fetch correct jira ticket number or etc.

Added `window.is_active_window()` guard in `picker.rs` -> `cancel` event

Release Notes:
- (Let's Git Together) Fixed a behavior where pickers would
automatically close upon the window becoming inactive.
2025-11-18 21:57:30 -05:00
Julia Ryan
1e2f15a3d7 Disable phpactor by default on windows (#43011)
We install phpactor by default, but on windows it doesn't work out of
the box (see
[here](https://github.com/phpactor/phpactor/discussions/2579) for
details). For now we'll default to using intelephense, but in the future
we'd like to switch back if phpactor lands windows support given that
it's open source.

Release Notes:

- N/A
2025-11-18 16:38:19 -08:00
Martin Bergo
7c0663b825 google_ai: Add gemini-3-pro-preview model (#43015)
Release Notes:

- Added the newly released Gemini 3 Pro Preview Model


https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-pro
2025-11-18 23:51:32 +00:00
Lukas Wirth
94a43dc73a extension_host: Fix IS_WASM_THREAD being set for wrong threads (#43005)
https://github.com/zed-industries/zed/pull/40883 implemented this
incorrectly. It was marking a random background thread as a wasm thread
(whatever thread picked up the wasm epoch timer background task),
instead of marking the threads that actually run the wasm extension.

This has two implications:
1. it didn't prevent extension panics from tearing down as planned
2. Worse, it actually made us hide legit panics in sentry for one of our
background workers.

Now 2 still technically applies for all tokio threads after this, but we
basically only use these for wasm extensions in the main zed binary.

Release Notes:

- Fixed extension panics crashing Zed on Linux
2025-11-18 23:49:22 +00:00
Ben Kunkle
e8e0707256 zeta2: Improve queries parsing (#43012)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Max <max@zed.dev>
2025-11-18 23:46:29 +00:00
Tom Zaspel
d7c340c739 docs: Add documenation for OpenTofu support (#42448)
Closes -

Release Notes:

- N/A

Signed-off-by: Tom Zaspel <40226087+tzabbi@users.noreply.github.com>
2025-11-18 18:40:09 -05:00
Julia Ryan
16b24e892e Increase error verbosity (#43013)
Closes #42288

This will actually print the parsing error that prevented the vscode
settings file from being loaded which should make it easier for users to
self help when they have an invalid config.

Release Notes:

- N/A
2025-11-18 23:25:12 +00:00
Barani S
917148c5ce gpui: Use DWM API for backdrop effects and add Mica/Mica Alt support (#41842)
This PR updates window background rendering to use the **official DWM
backdrop API** (`DwmSetWindowAttribute`) instead of the legacy
`SetWindowCompositionAttribute`.
It also adds **Mica** and **Mica Alt** options to
`WindowBackgroundAppearance` for native Windows 11 effects.

### Motivation

Enables modern, stable, and GPU-accelerated backdrops consistent with
Windows 11’s Fluent Design.
Removes reliance on undocumented APIs while maintaining backward
compatibility with older Windows versions.

### Changes

* Added `MicaBackdrop` and `MicaAltBackdrop` variants.
* Switched to DWM API for applying backdrop effects.
* Verified fallback behavior on Windows 10.

### Release Notes:

- Added `WindowBackgroundAppearance::MicaBackdrop` and
`WindowBackgroundAppearance::MicaAltBackdrop` for Windows 11 Mica and
Mica Alt window backdrops.

### Screenshots

- `WindowBackgroundAppearance::Blurred`
<img width="553" height="354" alt="image"
src="https://github.com/user-attachments/assets/57c9c25d-9412-4141-94b5-00000cc0b1ec"
/>

- `WindowBackgroundAppearance::MicaBackdrop`
<img width="553" height="354" alt="image"
src="https://github.com/user-attachments/assets/019f541c-3335-4c9e-b026-71f5a1786534"
/>

- `WindowBackgroundAppearance::MicaAltBackdrop`
<img width="553" height="354" alt="image"
src="https://github.com/user-attachments/assets/5128d600-c94d-4c89-b81a-8b842fe1337a"
/>

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2025-11-18 18:20:32 -05:00
Piotr Osiewicz
951132fc13 chore: Fix build graph - again (#42999)
11.3s -> 10.0s for silly stuff like extracting actions from crates.
project panel still depends on git_ui though..

Release Notes:

- N/A
2025-11-18 19:20:34 +01:00
Ben Kunkle
bf0dd4057c zeta2: Make new_text/old_text parsing more robust (#42997)
Closes #ISSUE

The model often uses the wrong closing tag, or has spaces around the
closing tag name. This PR makes it so that opening tags are treated as
authoritative and any closing tag with the name `new_text` `old_text` or
`edits` is accepted based on depth. This has the additional benefit that
the parsing is more robust with contents that contain `new_text`
`old_text` or `edits. I.e. the following test passes

```rust
    #[test]
    fn test_extract_xml_edits_with_conflicting_content() {
        let input = indoc! {r#"
            <edits path="component.tsx">
            <old_text>
            <new_text></new_text>
            </old_text>
            <new_text>
            <old_text></old_text>
            </new_text>
            </edits>
        "#};

        let result = extract_xml_replacements(input).unwrap();
        assert_eq!(result.file_path, "component.tsx");
        assert_eq!(result.replacements.len(), 1);
        assert_eq!(result.replacements[0].0, "<new_text></new_text>");
        assert_eq!(result.replacements[0].1, "<old_text></old_text>");
    }
```

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-18 12:36:37 -05:00
Conrad Irwin
3c4ca3f372 Remove settings::Maybe (#42933)
It's unclear how this would ever be useful

cc @probably-neb

Release Notes:

- N/A
2025-11-18 10:23:16 -07:00
Artur Shirokov
03132921c7 Add HTTP transport support for MCP servers (#39021)
### What this solves

This PR adds support for HTTP and SSE (Server-Sent Events) transports to
Zed's context server implementation, enabling communication with remote
MCP servers. Currently, Zed only supports local MCP servers via stdio
transport. This limitation prevents users from:

- Connecting to cloud-hosted MCP servers
- Using MCP servers running in containers or on remote machines
- Leveraging MCP servers that are designed to work over HTTP/SSE

### Why it's important

The MCP (Model Context Protocol) specification includes HTTP/SSE as
standard transport options, and many MCP server implementations are
being built with these transports in mind. Without this support, Zed
users are limited to a subset of the MCP ecosystem. This is particularly
important for:

- Enterprise users who need to connect to centralized MCP services
- Developers working with MCP servers that require network isolation
- Users wanting to leverage cloud-based context providers (e.g.,
knowledge bases, API integrations)

### Implementation approach

The implementation follows Zed's existing architectural patterns:

- **Transports**: Added `HttpTransport` and `SseTransport` to the
`context_server` crate, built on top of the existing `http_client` crate
- **Async handling**: Uses `gpui::spawn` for network operations instead
of introducing a new Tokio runtime
- **Settings**: Extended `ContextServerSettings` enum with a `Remote`
variant to support URL-based configuration
- **UI**: Updated the agent configuration UI with an "Add Remote Server"
option and dedicated modal for remote server management

### Changes included

- [x] HTTP transport implementation with request/response handling
- [x] SSE transport for server-sent events streaming
- [x] `build_transport` function to construct appropriate transport
based on URL scheme
- [x] Settings system updates to support remote server configuration
- [x] UI updates for adding/editing remote servers
- [x] Unit tests using `FakeHttpClient` for both transports
- [x] Integration tests (WIP)
- [x] Documentation updates (WIP)

### Testing

- Unit tests for both `HttpTransport` and `SseTransport` using mocked
HTTP client
- Manual testing with example MCP servers over HTTP/SSE
- Settings validation and UI interaction testing

### Screenshots/Recordings

[TODO: Add screenshots of the new "Add Remote Server" UI and
configuration modal]

### Example configuration

Users can now configure remote MCP servers in their `settings.json`:

```json
{
  "context_servers": {
    "my-remote-server": {
      "enabled": true,
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

### AI assistance disclosure

I used AI to help with:

- Understanding the MCP protocol specification and how HTTP/SSE
transports should work
- Reviewing Zed's existing patterns for async operations and suggesting
consistent approaches
- Generating boilerplate for test cases
- Debugging SSE streaming issues

All code has been manually reviewed, tested, and adapted to fit Zed's
architecture. The core logic, architectural decisions, and integration
with Zed's systems were done with human understanding of the codebase.
AI was primarily used as a reference tool and for getting unstuck on
specific technical issues.

Release notes:
* You can now configure MCP Servers that connect over HTTP in your
settings file. These are not yet available in the extensions API.
  ```
  {
    "context_servers": {
      "my-remote-server": {
        "enabled": true,
        "url": "http://localhost:3000/mcp"
      }
    }
  }
  ```

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-18 16:39:08 +00:00
Richard Feldman
c0fadae881 Thought signatures (#42915)
Implement Gemini API's [thought
signatures](https://ai.google.dev/gemini-api/docs/thinking#signatures)

Release Notes:

- Added thought signatures for Gemini tool calls
2025-11-18 10:41:19 -05:00
Agus Zubiaga
1c66c3991d Enable sweep flag for staff (#42987)
Release Notes:

- N/A
2025-11-18 15:39:27 +00:00
Agus Zubiaga
7e591a7e9a Fix sweep icon spacing (#42986)
Release Notes:

- N/A
2025-11-18 15:33:03 +00:00
Danilo Leal
c44d93745a agent_ui: Improve the modal to add LLM providers (#42983)
Closes https://github.com/zed-industries/zed/issues/42807

This PR makes the modal to add LLM providers a bit better to interact
with:

1. Added a scrollbar
2. Made the inputs navigable with tab
3. Added some responsiveness to ensure it resizes on shorter windows


https://github.com/user-attachments/assets/758ea5f0-6bcc-4a2b-87ea-114982f37caf

Release Notes:

- agent: Improved the modal to add LLM providers by making it responsive
and keyboard navigable.
2025-11-18 12:28:14 -03:00
Lukas Wirth
b4e4e0d3ac remote: Fix up incorrect logs (#42979)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-18 15:14:52 +00:00
Lukas Wirth
097024d46f util: Use process spawn helpers in more places (#42976)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-18 14:31:39 +00:00
Ben Brandt
f1c2afdee0 Update codex docs to include configuration for third-party providers (#42973)
Release Notes:

- N/A
2025-11-18 13:50:59 +00:00
Jakub Konka
ea120dfe18 Revert "git: Remove JobStatus from PendingOp in favour of in-flight p… (#42970)
…runing (#42955)"

This reverts commit 696fdd8fed.

Release Notes:

- N/A
2025-11-18 13:30:40 +00:00
Lukas Wirth
d2988ffc77 vim: Fix snapshot out of bounds indexing (#42969)
Fixes ZED-38X

Release Notes:

- N/A
2025-11-18 13:02:40 +00:00
Engin Açıkgöz
f17d2c92b6 terminal_view: Fix terminal opening in root directory when editing single file corktree (#42953)
Fixes #42945

## Problem
When opening a single file via command line (e.g., `zed
~/Downloads/file.txt`), the terminal panel was opening in the root
directory (/) instead of the file's directory.

## Root Cause
The code only checked for active project directory, which returns None
when a single file is opened. Additionally, file worktrees weren't
handling parent directory lookup.

## Solution
Added fallback logic to use the first project directory when there's no
active entry, and made file worktrees return their parent directory
instead of None.

## Testing
- All existing tests pass
- Added test coverage for file worktree scenarios
- Manually tested with `zed ~/Downloads/file.txt` - terminal now opens
in correct directory

This improves the user experience for users who frequently open single
files from the command line.

## Release Notes

- Fixed terminal opening in root directory when editing single files
from the command line
2025-11-18 13:37:48 +01:00
Antonio Scandurra
c1d9dc369c Try reducing flakiness of fs-event tests by bumping timeout to 4s on CI (#42960)
Release Notes:

- N/A
2025-11-18 11:00:02 +00:00
Jakub Konka
696fdd8fed git: Remove JobStatus from PendingOp in favour of in-flight pruning (#42955)
The idea is that we only store running (`!self.finished`) or finished
(`self.finished`) pending ops, while everything else (skipped, errored)
jobs are pruned out immediately. We don't really need them in the grand
scheme of things anyway.

Release Notes:

- N/A
2025-11-18 10:22:34 +00:00
Lena
980f8bff2a Add a github issue label to shoo the stalebot away (#42950)
Labeling an issue with "never stale" will keep the stalebot away; the
bot can get annoying in some situations otherwise.

Release Notes:

- N/A
2025-11-18 10:15:09 +01:00
Kirill Bulatov
2a3bcbfe0f Properly check chunk version on lsp store update (#42951)
Release Notes:

- N/A

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-18 09:13:32 +00:00
aleanon
5225a84aff For and await highlighting rust (#42924)
Closes #42922

Release Notes:

- Fixed Correctly highlighting the 'for' keyword in Rust as
keyword.control only in for loops.
- Fixed Highlighting the 'await' keyword in Rust as keyword.control
2025-11-18 09:11:36 +01:00
Max Brunsfeld
5c70f8391f Fix panic when using sweep AI without token env var (#42940)
Release Notes:

- N/A
2025-11-17 22:23:14 -08:00
Danilo Leal
10efbd5eb4 agent_ui: Show the "new thread" keybinding for the currently active agent (#42939)
This PR's goal is to improve discoverability of how Zed "remembers" the
currently selected agent when hitting `cmd-n` (or `ctrl-n`). Hitting
that binding starts a new thread with whatever agent is currently
selected.

In the example below, I am in a Claude Code thread and if I hit `cmd-n`,
a new, fresh CC thread will be started:

<img width="500" height="822" alt="Screenshot 2025-11-18 at 1  13@2x"
src="https://github.com/user-attachments/assets/d3acd1aa-459d-4078-9b62-bbac3b8c1600"
/>


Release Notes:

- agent: Improved discoverability of the `cmd-n` keybinding to create a
new thread with the currently selected agent.
2025-11-18 01:53:37 -03:00
Agus Zubiaga
0386f240a9 Add experimental Sweep edit prediction provider (#42927)
Only for staff

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-18 01:00:26 +00:00
Conrad Irwin
a39ba03bcc Use metrics-id for sentry user id when we have it (#42931)
This should make it easier to correlate Sentry reports with user reports
and
github issues (for users who have diagnostics enabled)

Release Notes:

- N/A
2025-11-17 23:44:59 +00:00
Lukas Wirth
2c7bcfcb7b multi_buffer: Work around another panic bug in path_key (#42920)
Fixes ZED-346 for now until I find the time to dig into this bug
properly

Release Notes:

- Fixed a panic in the diagnostics pane
2025-11-17 22:38:48 +00:00
Lukas Wirth
6bea23e990 text: Temporarily remove assert_char_boundary panics (#42919)
As discussed in the first responders meeting. We have collected a lot of
backtraces from these, but it's not quite clear yet what causes this.
Removing these should ideally make things a bit more stable even if we
may run into panics later one when the faulty anchor is used still.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 22:20:45 +00:00
Julia Ryan
98da1ea169 Fix remote extension syncing (#42918)
Closes #40906
Closes #39729

SFTP uploads weren't quoting the install directory which was causing
extension syncing to fail. We were also only running `install_extension`
once per remote-connection instead of once per project (thx @feeiyu for
pointing this out) so extension weren't being loaded in subsequently
opened remote projects.

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-17 16:13:58 -06:00
Danilo Leal
98a83b47e6 agent_ui: Make input fields in Bedrock settings keyboard navigable (#42916)
Closes https://github.com/zed-industries/zed/issues/36587

This PR enables jumping from one input to the other, in the Bedrock
settings section, with tab.

Release Notes:

- N/A
2025-11-17 19:13:15 -03:00
Danilo Leal
5f356d04ff agent_ui: Fix model name label truncation (#42921)
Closes https://github.com/zed-industries/zed/issues/32739

Release Notes:

- agent: Fixed an issue where the label for model names wouldn't use all
the available space in the model picker.
2025-11-17 19:12:26 -03:00
Marshall Bowers
73d3f9611e collab: Add external_id column to billing_customers table (#42923)
This PR adds an `external_id` column to the `billing_customers` table.

Release Notes:

- N/A
2025-11-17 22:12:00 +00:00
Finn Evers
d9cfc2c883 Fix formatting in various files (#42917)
This fixes various issues where rustfmt failed to format code due to too
long strings, most of which I stumbled across over the last week and
some additonal ones I searched for whilst fixing the others.

Release Notes:

- N/A
2025-11-17 21:48:09 +00:00
Dino
ee420d530e vim: Change approach to fixing vim's temporary mode bug (#42894)
The `Vim.exit_temporary_normal` method had been updated
(https://github.com/zed-industries/zed/pull/42742) to expect and
`Option<&Motion>` that would then be used to determine whether to move
the cursor right in case the motion was `Some(EndOfLine { ..})`.
Unfortunately this meant that all callers now had to provide this
argument, even if just `None`.

After merging those changes I remember that we could probably play
around with `clip_at_line_ends` so this commit removes those intial
changes in favor of updating the `vim::normal::Vim.move_cursor` method
so that, if vim is in temporary mode and `EndOfLine` is used, it
disables clipping at line ends so that the newline character can be
selected.

Closes [#42278](https://github.com/zed-industries/zed/issues/42278)

Release Notes:

- N/A
2025-11-17 21:34:37 +00:00
Miguel Raz Guzmán Macedo
d801d0950e Add @miguelraz to reviewers and support sections (#42904)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 15:22:34 -06:00
Lukas Wirth
3f25d36b3c agent_ui: Fix text pasting no longer working (#42914)
Regressed in https://github.com/zed-industries/zed/pull/42908
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 21:04:50 +00:00
Joseph T. Lyons
f015368586 Update top-ranking issues script (#42911)
- Added Windows category
- Removed unused import
- Fixed a type error reported by `ty`

Release Notes:

- N/A
2025-11-17 15:20:22 -05:00
Ben Kunkle
4bf3b9d62e zeta2: Output bucketed_analysis.md (#42890)
Closes #ISSUE

Makes it so that a file named `bucketed_analysis.md` is written to the
runs directory after an eval is ran with > 1 repetitions. This file
buckets the predictions made by the model by comparing the edits made so
that seeing how many times different failure modes were encountered
becomes much easier.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 15:17:39 -05:00
Lukas Wirth
599a217ea5 workspace: Fix logging of errors in prompt_err (#42908)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 20:39:50 +01:00
ozzy
b0a7defd09 Fix track file renames in git panel (#42352)
Closes #30549

Release Notes:

- Fixed: Git renames now properly show as renamed files in the git panel
instead of appearing as deleted + untracked files
<img width="351" height="132" alt="Screenshot 2025-11-10 at 17 39 44"
src="https://github.com/user-attachments/assets/80e9c286-1abd-4498-a7d5-bd21633e6597"
/>
<img width="500" height="95" alt="Screenshot 2025-11-10 at 17 39 55"
src="https://github.com/user-attachments/assets/e4c59796-df3a-4d12-96f4-e6706b13a32f"
/>
2025-11-17 13:25:51 -06:00
Davis Vaughan
57e3bcfcf8 Revise R documentation - about Air in particular (#42755)
Returning the favor from @rgbkrk in
https://github.com/posit-dev/air/pull/445

I noticed the R docs around Air are a bit incorrect / out of date. I'll
make a few more comments inline. Feel free to take over for any other
edits.

Release Notes:

- Improved R language support documentation
2025-11-17 11:53:42 -07:00
Oleksiy Syvokon
b2f561165f zeta2: Support qwen3-minimal prompt format (#42902)
This prompt is for a fine-tuned model. It has the following changes,
compared to `minimal`:
- No instructions at all, except for one sentence at the beginning of
the prompt.
- Output is a simplified unified diff -- hunk headers have no line
counts (e.g., `@@ -20 +20 @@`)
- Qwen's FIM tokens are used where possible (`<|file_sep|>`,
`<|fim_prefix|>`, `<|fim_suffix|>`, etc.)

To evaluate this model:
```
ZED_ZETA2_MODEL=zeta2-exp [usual zeta-cli eval params ...]  --prompt-format minimal-qwen
```

This will point to the most recent Baseten deployment of zeta2-exp
(which may change in the future, so the prompt-format may get out of
sync).

Release Notes:

- N/A
2025-11-17 20:36:05 +02:00
localcc
fd1494c31a Fix remote server completions not being queried from all LSP servers (#42723)
Closes #41294

Release Notes:

- Fixed remote LSPs not being queried
2025-11-17 18:07:49 +00:00
Danilo Leal
faa1136651 agent_ui: Don't create a new terminal when hitting the new thread binding from the terminal (#42898)
Closes https://github.com/zed-industries/zed/issues/32701

Release Notes:

- agent: Fixed a bug where hitting the `NewThread` keybinding when
focused inside a terminal within the agent panel would create a new
terminal tab instead of a new thread.
2025-11-17 15:05:38 -03:00
Conrad Irwin
6bf5e92a25 Revert "Keep selection in SwitchToHelixNormalMode (#41583)" (#42892)
Closes #ISSUE

Release Notes:

- Fixes vim "go to definition" making a selection
2025-11-17 11:01:34 -07:00
Lukas Wirth
46ad6c0bbb ci: Remove remaining nextest compiles (#42630)
Follow up to https://github.com/zed-industries/zed/pull/42556

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 17:59:48 +00:00
Lukas Wirth
671500de1b agent_ui: Fix images copied from win explorer not being pastable (#42858)
Closes https://github.com/zed-industries/zed/issues/41505

A bit adhoc but it gets the job done for now

Release Notes:

- Fixed images copied from windows explorer not being pastable in the
agent panel
2025-11-17 17:58:22 +00:00
Kirill Bulatov
0519c645fb Deduplicate inlays when getting those from multiple language servers (#42899)
Part of https://github.com/zed-industries/zed/issues/42671

Release Notes:

- Deduplicate inlay hints from different language servers
2025-11-17 17:53:05 +00:00
Richard Feldman
23872b0523 Fix stale edits (#42895)
Closes #34069

<img width="532" height="880" alt="Screenshot 2025-11-17 at 11 14 19 AM"
src="https://github.com/user-attachments/assets/abc50c32-d54d-4310-a6e6-83008db7ed81"
/>

<img width="525" height="863" alt="Screenshot 2025-11-17 at 12 22 50 PM"
src="https://github.com/user-attachments/assets/15a69792-c2c7-4727-add9-c1f9baa5e665"
/>

Release Notes:

- Agent file edits now error if the file has changed since last read
(allowing the agent to read changes and avoid overwriting changes made
outside Zed)
2025-11-17 12:23:18 -05:00
Richard Feldman
4b050b651a Support Agent Servers on remoting (#42683)
<img width="348" height="359" alt="Screenshot 2025-11-13 at 6 53 39 PM"
src="https://github.com/user-attachments/assets/6fe75796-8ceb-4f98-9d35-005c90417fd4"
/>

Also added support for per-target env vars to Agent Server Extensions

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

Release Notes:

- Per-target env vars are now supported on Agent Server Extensions
- Agent Server Extensions are now available when doing SSH remoting

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-17 10:48:14 -05:00
Danilo Leal
bb46bc167a settings_ui: Add "Edit in settings.json" button to subpage header (#42886)
Closes https://github.com/zed-industries/zed/issues/42094

This will make it consistent with the regular/main page. Also ended up
fixing a bug along the way where this button wouldn't work for subpage
items.

Release Notes:

- settings ui: Fixed a bug where the "Edit in settings.json" wouldn't
work for subpages like all the Language pages.
2025-11-17 11:58:43 -03:00
Oleksiy Syvokon
b274f80dd9 zeta2: Print average length of prompts and outputs (#42885)
Release Notes:

- N/A
2025-11-17 16:56:58 +02:00
Danilo Leal
d77ab99ab1 keymap_editor: Make "toggle exact match mode" the default for binding search (#42883)
I think having the "exact mode" turned on by default is usually what
users will expect when searching for a specific keybinding. When it's
turned off, it's very odd to search for a super common binding like
"command-enter" and get no results. That happens because without that
mode, we're trying to match for subsequent matches, which I'm betting
it's an edge case. Hopefully, this change will make the keymap editor
feel more like it works well.

I'm also adding the toggle icon button inside the keystroke input for
consistency with the project search input.

Making this change very inspired by [Sam Rose's
feedback](https://bsky.app/profile/samwho.dev/post/3m5juszqyd22w).

Release Notes:

- keymap editor: Made the "toggle exact match mode" the default
keystroke search mode so that whatever you search for matches exactly to
results.
2025-11-17 11:43:15 -03:00
Finn Evers
97792f7fb9 Prefer loading extension.toml before extension.json (#42884)
Closes #42406

The issue for the fish-extension is that a `extension.json` is still
present next to a `extension.toml`, although the former is deprecated.

We should prefer the `extension.toml` if it is present and only fall
back to the `extension.json` if needed. This PR tackles this.

Release Notes:

- N/A
2025-11-17 14:29:50 +00:00
tidely
9bebf314e0 http_client: Remove unused HttpClient::type_name method (#42803)
Closes #ISSUE

Remove unused method `HttpClient::type_name`. Looking at the PR from a
year ago when it was added, it was never actually used for anything and
seems like a prototyping artifact.

Other misc changes for the `http_client` crate include:

- Use `derive_more::Deref` for `HttpClientWithUrl` (already used for
`HttpClientWithProxy`)
- Move `http_client::proxy()` higher up in the trait definition. (It was
in between methods that have default implementations)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 15:28:22 +01:00
Danilo Leal
4092e81ada keymap_editor: Adjust some items of the UI (#42876)
- Only showing the "Create" menu item in the right-click context menu
for actions that _do not_ contain a binding already assigned to them
- Only show the "Clear Input" icon button in the keystroke modal when
the input is focused/in recording mode
- Add a subtle hover style to the table rows just to make it easier to
navigate

Release Notes:

- N/A
2025-11-17 10:59:46 -03:00
Kirill Bulatov
e0b64773d9 Properly sanitize out inlay hints from remote hosts (#42878)
Part of https://github.com/zed-industries/zed/issues/42671

Release Notes:

- Fixed remote hosts causing duplicate hints to be displayed
2025-11-17 15:53:18 +02:00
Piotr Osiewicz
f1bebd79d1 zeta2: Add skip-prediction flag to eval CLI (#42872)
Release Notes:

- N/A
2025-11-17 13:37:51 +00:00
Lukas Wirth
a66a539a09 Reduce macro burden for rust-analyzer (#42871)
This enables optimizations for our own proc-macros as well as some heavy
hitters. Additionally this gates the `derive_inspector_reflection` to be
skipped for rust-analyzer as it currently slows down rust-analyzer way
too much

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-17 12:31:00 +00:00
Lucas Parry
a2d3e3baf9 project_panel: Add sort mode (#40160)
Closes #4533 (partly at least)

Release Notes:

- Added `project_panel.sort_mode` option to control explorer file sort
(directories first, mixed, files first)

 ## Summary

Adds three sorting modes for the project panel to give users more
control over how files and directories are displayed:

- **`directories_first`** (default): Current behaviour - directories
grouped before files
- **`mixed`**: Files and directories sorted together alphabetically
- **`files_first`**: filed grouped before directories

 ## Motivation

Users coming from different editors and file managers have different
expectations for file sorting. Some prefer directories grouped at the
top (traditional), while others prefer the macOS Finder-style mixed
sorting where "Apple1/", "apple2.tsx" and "Apple3/" appear
alphabetically mixed together.


 ### Screenshots

New sort options in settings:
<img width="515" height="160" alt="image"
src="https://github.com/user-attachments/assets/8f4e6668-6989-4881-a9bd-ed1f4f0beb40"
/>


Directories first | Mixed | Files first
-------------|-----|-----
<img width="328" height="888" alt="image"
src="https://github.com/user-attachments/assets/308e5c7a-6e6a-46ba-813d-6e268222925c"
/> | <img width="327" height="891" alt="image"
src="https://github.com/user-attachments/assets/8274d8ca-b60f-456e-be36-e35a3259483c"
/> | <img width="328" height="890" alt="image"
src="https://github.com/user-attachments/assets/3c3b1332-cf08-4eaf-9bed-527c00b41529"
/>


### Agent usage

Copilot-cli/claude-code/codex-cli helped out a lot. I'm not from a rust
background, but really wanted this solved, and it gave me a chance to
play with some of the coding agents I'm not permitted to use for work
stuff

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-17 17:52:46 +05:30
Serophots
175162af4f project_panel: Fix preview tabs disabling focusing files after just one click in project panel (#42836)
Closes #41484

With preview tabs disabled, when you click once on a file in the project
panel, rather than focusing on that file, zed will incorrectly focus on
the text editor panel. This means if you click on a file to focus it,
then follow up with a keybind like backspace to delete that file, it
doesn't delete that file because the backspace goes through to the text
editor instead.

Incorrect behaviour seen here:


https://github.com/user-attachments/assets/8c2dea90-bd90-4507-8ba6-344be348f151



Release Notes:

- Fixed improper UI focus behaviour in the project panel when preview
tabs are disabled
2025-11-17 16:53:12 +05:30
Dino
cdcc068906 vim: Fix temporary mode exit on end of line (#42742)
When using the end of line motion ($) while in temporary mode, the
cursor would be placed in insert mode just before the last character
instead of after, just like in NeoVim.

This happens because `EndOfLine` kind of assumes that we're in `Normal`
mode and simply places the cursor in the last character instead of the
newline character.

This commit moves the cursor one position to the right when exiting
temporary mode and the motion used was `Motion::EndOfLine`

- Update `vim::normal::Vim.exit_temporary_normal` to now accept a
`Option<&Motion>` argument, in case callers want this new logic to
potentially be applied

Closes #42278 

Release Notes:

- Fixed temporary mode exit when using `$` to move to the end of the
line
2025-11-17 11:14:49 +00:00
Mayank Verma
86484aaded languages: Clean up invalid init calls after recent API changes (#42866)
Related to https://github.com/zed-industries/zed/pull/41670

Release Notes:

- Cleaned up invalid init calls after recent API changes in
https://github.com/zed-industries/zed/pull/42238
2025-11-17 10:29:29 +00:00
Mayank Verma
d32934a893 languages: Fix indentation for if/else statements in C/C++ without braces (#41670)
Closes #41179

Release Notes:

- Fixed indentation for if/else statements in C/C++ without braces
2025-11-17 10:05:54 +01:00
warrenjokinen
b463266fa1 Remove mention of Fireside Hacks (#42853)
Fireside Hack events are no longer being held.

Closes #ISSUE

Release Notes:

- N/A
2025-11-16 23:26:38 -05:00
Agus Zubiaga
b0525a26a6 Report automatically discarded zeta predictions (#42761)
We weren't reporting predictions that were generated but never made it
out of the provider, such as predictions that failed to interpolate, and
those that are cancelled because another request completes before it.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-16 11:51:13 -08:00
Smit Barmase
1683052e6c editor: Fix MoveToEnclosingBracket and unmatched forward/backward Vim motions in Markdown code blocks (#42813)
We now correctly use bracket ranges from the deepest syntax layer when
finding enclosing brackets.

Release Notes:

- Fixed an issue where `MoveToEnclosingBracket` didn’t work correctly
inside Markdown code blocks.
- Fixed an issue where unmatched forward/backward Vim motions didn’t
work correctly inside Markdown code blocks.

---------

Co-authored-by: MuskanPaliwal <muskan10112002@gmail.com>
2025-11-15 23:57:49 +05:30
Alvaro Parker
07cc87b288 Fix wild install script (#42747)
Use
[`command`](https://www.gnu.org/software/bash/manual/bash.html#index-command)
instead of `which` to check if `wild` is installed.

Using `which` will result in an error being printed to stdout: 

```bash
./script/install-wild
which: invalid option -- 's'
/usr/local/bin/wild
Warning: existing wild 0.6.0 found at /usr/local/bin/wild. Skipping installation.
```

Release Notes:

- N/A
2025-11-15 15:15:37 +01:00
Danilo Leal
1277f328c4 docs: Improve custom keybinding for external agent example (#42776)
Follow up to https://github.com/zed-industries/zed/pull/42772 adding
some comments to improve clarity.

Release Notes:

- N/A
2025-11-14 23:08:46 +00:00
Danilo Leal
b3097cfc8a docs: Add section about keybinding for external agent threads (#42772)
Release Notes:

- N/A
2025-11-14 19:54:52 -03:00
Ivan Pasquariello
305206fd48 Make drag and double click enabled on the whole title bar on macOS (#41839)
Closes #4947

Taken inspiration from @tasuren implementation, plus the addition for
the double click enabled on the whole title bar too to
maximizes/restores the window.

I was not able to test the application on Linux, no need to test on
Windows since the feature is enabled by the OS.

Release Notes:

- Fixed title bar not fully draggable on macOS
- Fixed not being able to maximizes/restores the window with double
click on the whole title bar on macOS
2025-11-14 22:46:35 +00:00
Ben Kunkle
c387203ac8 zeta2: Prediction prompt engineering (#42758)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-11-14 16:50:55 -05:00
Danilo Leal
a260ba6428 agent_ui: Simplify labels in new thread menu (#42746)
Drop the "new", it's simpler! 😆 

| Before | After |
|--------|--------|
| <img width="800" height="932" alt="Screenshot 2025-11-14 at 2  48@2x"
src="https://github.com/user-attachments/assets/efa67d57-9b5c-4eef-8dc7-f36c8e6a4a90"
/> | <img width="800" height="772" alt="Screenshot 2025-11-14 at 2 
47@2x"
src="https://github.com/user-attachments/assets/042d2a0b-24b4-4ad5-8411-82e0eafb993f"
/> |




Release Notes:

- N/A
2025-11-14 18:02:09 +00:00
Lukas Wirth
a8e0de37ac gpui: Fix crashes when losing devices while resizing on windows (#42740)
Fixes ZED-1HC

Release Notes:

- Fixed Zed panicking when moving Zed windows over different screens
associated with different gpu devices on windows
2025-11-14 17:51:26 +00:00
Danilo Leal
a1a599dac5 collab_ui: Fix search matching in the panel (#42743)
Release Notes:

- collab: Fixed a regression where search matches wouldn't expand the
parent channel if that happened to be collapsed.
2025-11-14 14:45:58 -03:00
Smit Barmase
524b97d729 project_panel: Fix autoscroll and filename editor focus race condition (#42739)
Closes https://github.com/zed-industries/zed/issues/40867

Since the recent changes in
[https://github.com/zed-industries/zed/pull/38881](https://github.com/zed-industries/zed/pull/38881),
the filename editor is sometimes not focused after duplicating a file or
creating a new one, and similarly, autoscroll sometimes didn’t work. It
turns out that multiple calls to `update_visible_entries_task` cancel
the existing task, which might contain information about whether we need
to focus the filename editor and autoscroll after the task ends. To fix
this, we now carry that information forward to the next task that
overwrites it, so that when the latest task ends, we can use that
information to do the right thing.

Release Notes:

- Fixed an issue in the Project Panel where duplicating or creating an
entry sometimes didn’t focus the rename editing field.
2025-11-14 21:56:48 +05:30
Ben Kunkle
8772727034 zeta2: Improve zeta old text matching (#42580)
This PR improves Zeta2's matching of `old_text`/`new_text` pairs, using
similar code to what we use in the edit agent. For right now, we've
duplicated the code, as opposed to trying to generalize it.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Agus <agus@zed.dev>
2025-11-14 11:18:16 -05:00
Dino
aaa116d129 languages: Fix command used for Go subtests (#42734)
The command used to run go subtests was breaking if the test contained
square brackets, for example:

```
go test . -v -run ^TestInventoryCheckout$/^\[test\]_test_checkout$
```

After a bit of testing it appears that the best way to actually resolve
this in a way supported by `go test` is to wrap this command in quotes.
As such, this commit updates the command to, considering the example
above:

```
go test . -v -run '^TestInventoryCheckout$/^\[test\]_test_checkout$'
```

We also tested escape the square brackets, using `\\\[` instead of `\[`,
but that would lead to a more complex change, so we opted for the
simpler solution of wrapping the command in quotes.

Closes #42347 

Release Notes:

- Fixed command used to run Go subtests to ensure that escaped
characters don't lead to a failure in finding tests to run
2025-11-14 16:04:54 +00:00
Danilo Leal
c1096d8b63 agent_ui: Render error descriptions as markdown in thread view callouts (#42732)
This PR makes the description in the callout that display general errors
in the agent panel be rendered as markdown. This allow us to pass URLs
to these error strings that will be clickable, improving the overall
interaction with them. Here's an example:

<img width="500" height="396" alt="Screenshot 2025-11-14 at 11  43@2x"
src="https://github.com/user-attachments/assets/f4fc629a-6314-4da1-8c19-b60e1a09653b"
/>

Release Notes:

- agent: Improved the interaction with errors by allowing links to be
clickable.
2025-11-14 12:12:47 -03:00
Josh Piasecki
092071a2f0 git_ui: Allow opening a file with the diff hunks expanded (#40616)
So i just discovered `editor::ExpandAllDiffHunks`

I have been really missing the ability to look at changes NOT in a multi
buffer so i was very pleased to finally figure out that this is already
possible in Zed.

i have seen alot of discussion/issues requesting this feature so i think
it is safe to say i'm not the only one that is not aware it exists.

i think the wording in the docs could better communicate what this
feature actually is, however, i think an even better way to show users
that this feature exists would be to just put it in front of them.

In the `GitPanel`:
- `menu::Confirm` opens the project diff
- `menu::SecondaryConfirm` opens the selected file in a new editor.

I think it would be REALLY nice if opening a file with
`SecondaryConfirm` opened the file with the diff hunks already expanded
and scrolled the editor to the first hunk.

ideally i see this being toggle-able in settings something like
`GitPanel - Open File with Diffs Expanded` or something. so the user
could turn this off if they preferred.

I tried creating a new keybinding using the new `actions::Sequence`
it was something like:
```json
{
  "context": "GitPanel && ChangesList",
  "bindings": {
    "cmd-enter" : [ "actions::Sequence", ["menu:SecondaryConfirm", "editor::ToggleFocus", "editor::ExpandAllDiffHunks", "editor::GoToHunk"]]
  }
}
```
but the action sequence does not work. i think because opening the file
is an async task.

i have a first attempt here, of just trying to get the diff hunks to
expand after opening the file.
i tried to copy and paste the logic/structure as best i could from the
confirm method in file_finder.rs:1432

it compiles, but it does not work, and i do not have enough experience
in rust or in this project to figure out anything further.

if anyone was interested in working on this with me i would enjoy
learning more and i think this would be a nice way to showcase this
tool!
2025-11-14 08:47:46 -05:00
Oleksiy Syvokon
723f9b1371 zeta2: Add minimal prompt for fine-tuned models (#42691)
1. Add `--prompt-format=minimal` that matches single-sentence
instructions used in fine-tuned models (specifically, in `1028-*` and
`1029-*` models)

2. Use separate configs for agentic context search model and edit
prediction model. This is useful when running a fine-tuned EP model, but
we still want to run vanilla model for context retrieval.

3. `zeta2-exp` is a symlink to the same-named Baseten deployment. This
model can be redeployed and updated without having to update the
deployment id.

4. Print scores as a compact table

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
2025-11-14 13:08:54 +00:00
Jakub Konka
37523b0007 git_panel: Fix buffer header checkbox not showing partially staged files (#42718)
Release Notes:

- Fixed buffer header controls (staging checkbox) not showing partially
staged files
2025-11-14 12:55:04 +00:00
Jakub Konka
b4167caaf1 git_panel: Fix StageAll/UnstageAll not working when panel not in focus (#42708)
Release Notes:

- Fixed "Stage All"/"Unstage All" buttons from not working when git
panel is not in focus
2025-11-14 10:42:32 +01:00
Smit Barmase
020f518231 project_panel: Add tests for cross worktree drag-and-drop (#42704)
Add missing tests for cross worktree drag-and-drop:

- file -> directory
- file -> file (drops into parent directory)
- whole directory move
- multi selection move

Release Notes:

- N/A
2025-11-14 13:31:44 +05:30
morgankrey
ead4f26b52 Update docs for Gemini ZDR (#42697)
Closes #ISSUE

Release Notes:

- N/A
2025-11-14 00:22:20 -06:00
Josh Piasecki
3de3a369f5 editor: Add diffs_expanded to key context when diff hunks are expanded (#40617)
including a new identifier on the Editor key context will allow for some
more flexibility when creating keybindings.

for example i would like to be able to set the following:
```json
{
  "context": "Editor",
  "bindings": {
    "pageup": ["editor::MovePageUp", { "center_cursor": true }],
    "pagedown": ["editor::MovePageDown", { "center_cursor": true }],
  }
},
{
  "context": "Editor && diffs_expanded",
  "bindings": {
    "pageup": "editor::GoToPrevHunk",
    "pagedown": "editor::GoToHunk",
  }
},
```

<img width="1392" height="1167" alt="Screenshot 2025-10-18 at 23 51 46"
src="https://github.com/user-attachments/assets/cf4e262e-97e7-4dd9-bbda-cd272770f1ac"
/>


very open to suggestions for the name. that's the best i could come up
with.

the action *IS* called `editor::ExpandAllDiffHunks` so this seems
fitting.

the identifier is included if *any* diff hunk is visible, even if some
of them have been closed using `editor::ToggleSelectedDiffHunk`


Release Notes:

- The Editor key context now includes 'diffs_expanded' when diff changes
are visible
2025-11-14 03:33:53 +00:00
Xipeng Jin
28a0b82618 git_panel: Fix FocusChanges does nothing with no entries (#42553)
Closes #31155

Release Notes:

- Ensure `git_panel::FocusChanges` bypasses the panel’s `Focusable`
logic and directly focuses the `ChangesList` handle so the command works
even when the repository has no entries.
- Keep the `Focusable` behavior from the commit 45b126a (which routes
empty panels to the commit editor) by handling this special-case action
rather than regressing the default focus experience.
2025-11-13 21:59:39 -05:00
Mayank Verma
e2c95a8d84 git: Continue parsing other branches when refs have missing fields (#42523)
Closes #34684

Release Notes:

- (Let's Git Together) Fixed Git panel not showing any branches when
repository contains refs with missing fields
2025-11-13 21:16:38 -05:00
Anthony Eid
3da4d3aac3 settings_ui: Make open project settings action open settings UI (#42669)
This PR makes the `OpenProjectSettings` action open the settings UI in
project settings mode for the first visible worktree, instead of opening
the file. It also adds a `OpenProjectSettingsFile` action that maintains
the old behavior.

Finally, this PR partially fixes a bug where the settings UI won't load
project settings when the settings window is loaded before opening a
project/workspace. This happens because the global `app_state` isn't
correct in the `Subscription` that refreshes the available setting files
to open. The bug is still present in some cases, but it's out of scope
for this PR.

Release Notes:

- settings ui: Project Settings action now opens settings UI instead of
a file
2025-11-13 20:06:09 -05:00
Conrad Irwin
6f99eeffa8 Don't try and delete ./target/. (#42680)
Release Notes:

- N/A
2025-11-13 16:39:35 -07:00
Julia Ryan
15ab96af6b Add windows nightly update banner (#42576)
Hopefully this will nudge some of the beta users who were on nightly to
get on the official stable builds now that they're out.

Release Notes:

- N/A
2025-11-13 15:33:33 -08:00
Marshall Bowers
e80b490ac0 client: Clear plan and usage information when signing out (#42678)
This PR makes it so we clear the user's plan and usage information when
they sign out.

Release Notes:

- Signing out will now clear the local cache containing the plan and
usage information.
2025-11-13 23:13:27 +00:00
Jakub Konka
3c577ba019 git_panel: Fix Stage All/Unstage All ignoring partially staged files (#42677)
Release Notes:

- Fix "Stage All"/"Unstage All" not affecting partially staged files
2025-11-13 23:57:05 +01:00
Danilo Leal
e1d295a6b4 markdown: Improve table display (#42674)
Closes https://github.com/zed-industries/zed/issues/36330
Closes https://github.com/zed-industries/zed/issues/35460

This PR improves how we display markdown tables by relying on grids
rather than flexbox. Given this makes text inside each cell wrap, I
ended up removing the `table_overflow_x_scroll` method, as it was 1)
used only in the agent panel, and 2) arguably not the best approach as a
whole, because as soon as you need to scroll a table, you probably need
more elements to make it be really great.

One thing I'm slightly unsatisfied with, though, is the border
situation. I added a half pixel border to the cell so they all sum up to
1px, but there are cases where there's a tiny space between rows and I
don't quite know where that's coming from and how it happens. But I
think it's a reasonable improvement overall.

<img width="500" height="1248" alt="Screenshot 2025-11-13 at 7  05@2x"
src="https://github.com/user-attachments/assets/182b2235-efeb-4a61-ada2-98262967355d"
/>

Release Notes:

- agent: Improved table rendering in the agent panel, ensuring cell text
wraps, not going off-screen.
2025-11-13 19:36:16 -03:00
AidanV
84f24e4b62 vim: Add :<range>w <filename> command (#41256)
Release Notes:

- Adds support for `:[range]w {file}`
  - This writes the lines in the range to the specified
- Adds support for `:[range]w`
  - This replaces the current file with the selected lines
2025-11-13 13:27:08 -07:00
Abul Hossain Khan
03fad4b951 workspace: Fix pinned tab causing resize loop on adjacent tab (#41884)
Closes #41467 

My first PR in Zed, any guidance or tips are appreciated.

This fixes the flickering/resize loop that occurred on the tab
immediately to the right of a pinned tab.

Removed the conditional border on the pinned tabs container. The border
was a visual indicator to show when unpinned tabs were scrolled, but it
wasn't essential and was causing the layout thrashing.

Release Notes:

- Fixed

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-14 01:52:57 +05:30
Kevin Rubio
c626e770a0 outline_panel: Remove toggle expanded behavior from OpenSelectedEntry (#42214)
Fixed outline panel space key behavior by removing duplicate toggle call

The `open_selected_entry` function in `outline_panel.rs` was incorrectly
calling `self.toggle_expanded(&selected_entry, window, cx)` in addition
to its primary logic, causing the space key to both open/close entries
AND toggle their expanded state. Removed the redundant `toggle_expanded`
call to achieve the intended behavior.

Closes #41711

Release Notes:

- Fixed issue with the outline panel where pressing space would cause an
open selected entry to collapse and cause a closed selected entry to
open.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-14 01:07:22 +05:30
Lionel Henry
fa0c7500c1 Update runtimed to fix compatibility issue with the Ark kernel (#40889)
Closes #40888

This updates runtimed to the latest version, which handles the
"starting" variant of `execution_state`. It actually handles a bunch of
other variants that are not documented in the protocol (see
https://jupyter-client.readthedocs.io/en/stable/messaging.html#kernel-status),
like "starting", "terminating", etc. I added implementations for these
variants as well.

Release Notes:

- Fixed issue that prevented the Ark kernel from working in Zed
(#40888).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-13 19:35:45 +00:00
Richard Feldman
e91be9e98e Fix ACP CLI login via remote (#42647)
Release Notes:

- Fixed logging into Gemini CLI and Claude Code when remoting and
authenticating via CLI

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-11-13 19:13:09 +01:00
Rafael Lüder
46eb9e5223 Update scale factor and drawable size when macOS window changes screen (#38269)
Summary

Fixes UI scaling issue that occurs when starting Zed after disconnecting
an external monitor on macOS. The window's scale factor and drawable
size are now properly updated when the window changes screens.

Problem Description

When an external monitor is disconnected and Zed is started with only
the built-in screen active, the UI scale becomes incorrect. This happens
because:

1. macOS triggers the `window_did_change_screen` callback when a window
moves between displays (including when displays are disconnected)
2. The existing implementation only restarted the display link but
didn't update the window's scale factor or drawable size
3. This left the window with stale scaling information from the previous
display configuration

Root Cause

The `window_did_change_screen` callback in
`crates/gpui/src/platform/mac/window.rs` was missing the logic to update
the window's scale factor and drawable size when moving between screens.
This logic was only present in the `view_did_change_backing_properties
callback`, which isn't triggered when external monitors are
disconnected.

Solution

- Extracted common logic: Created a new `update_window_scale_factor()`
function that encapsulates the scale factor and drawable size update
logic
- Added scale factor update to screen change: Modified
`window_did_change_screen` to call this function after restarting the
display link
- Refactored existing code: Updated `view_did_change_backing_properties`
to use the new shared function, reducing code duplication

The fix ensures that whenever a window changes screens (due to monitor
disconnect, reconnect, or manual movement), the scale factor, drawable
size, and renderer state are properly synchronized.

Testing

-  Verified that UI scaling remains correct after disconnecting
external monitor
-  Confirmed that reconnecting external monitor works properly
-  Tested that manual window movement between displays updates scaling
correctly
-  No regressions observed in normal window operations

To verity my fix worked I had to copy my preview workspace over my dev
workspace, once I had done this I could reproduce the issue on main
consistently. After switching to the branch with this fix the issue was
resolved.

The fix is similar to what was done on
https://github.com/zed-industries/zed/pull/35686 (Windows)

Closes #37245 #38229

Release Notes:

- Fixed: Update scale factor and drawable size when macOS window changes
screen

---------

Co-authored-by: Kate <work@localcc.cc>
2025-11-13 16:51:13 +00:00
Conrad Irwin
cb7bd5fe19 Include source PR number in cherry-picks (#42642)
Release Notes:

- N/A
2025-11-13 16:06:26 +00:00
Ben Kunkle
b900ac2ac7 ci: Fix script/clear-target-dir-if-larger-than post #41652 (#42640)
Closes #ISSUE

The namespace runners mount the `target` directory to the cache drive,
so `rm -rf target` would fail with `Device Busy`. Instead we now do `rm
-rf target/* target/.*` to remove all files (including hidden files)
from the `target` directory, without removing the target directory
itself

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-13 10:58:59 -05:00
Dino
b709996ec6 editor: Fix pane's tab buttons flicker on right-click (#42549)
Whenever right-click was used on the editor, the pane's tab buttons
would flicker, which was confirmed to happen because of the following
check:

```
self.focus_handle.contains_focused(window, cx)
    || self
        .active_item()
        .is_some_and(|item| {
            item.item_focus_handle(cx).contains_focused(window, cx)
        })
```

This check was returning `false` right after right-clicking but
returning `true` right after. When digging into it a little bit more,
this appears to be happening because the editor's `MouseContextMenu`
relies on `ContextMenu` which is rendered in a deferred fashion but
`MouseContextMenu` updates the window's focus to it instantaneously.

Since the `ContextMenu` is rendered in a deferred fashion, its focus
handle is not yet a descendant of the editor (pane's active item) focus
handle, so the `contains_focused(window, cx)` call would return `false`,
with it returning `true` after the menu was rendered.

This commit updates the `MouseContextMenu::new` function to leverage
`cx.on_next_frame` and ensure that the focus is only moved to the
`ContextMenu` 2 frames later, ensuring that by the time the focus is
moved, the `ContextMenu`'s focus handle is a descendant of the editor's.

Closes #41771 

Release Notes:

- Fixed pane's tab buttons flickering when using right-click on the
editor
2025-11-13 15:57:26 +00:00
Smit Barmase
b6972d70a5 editor: Fix panic when calculating jump data for buffer header (#42639)
Just on nightly.

Release Notes:

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

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-11-13 15:48:05 +00:00
kitt
ec1664f61a zed: Enable line wrapping for cli help (#42496)
This enables clap's [wrap-help] feature and sets max_term_width to wrap
after 100 columns (the value clap is planning to default to in clap-v5).

This commit also adds blank lines which cause clap to split longer doc
comments into separate help (displayed for `-h`) and long_help
(displayed for `--help`) messages, as per [doc-processing].

[wrap-help]:
https://docs.rs/clap/4.5.49/clap/_features/index.html#optional-features
[doc-processing]:
https://docs.rs/clap/4.5.49/clap/_derive/index.html#pre-processing

![before: some lines of help text stretch across the whole screen.
after: all lines are wrapped at 100 columns, and some manual linebreaks
are preserved where it makes sense (in particular, when listing the
user-data-dir locations on each
platform)](https://github.com/user-attachments/assets/359067b4-5ffb-4fe3-80bd-5e1062986417)


Release Notes:

- N/A
2025-11-13 10:46:51 -05:00
Agus Zubiaga
c2c5fceb5b zeta eval: Allow no headings under "Expected Context" (#42638)
Release Notes:

- N/A
2025-11-13 15:43:22 +00:00
Richard Feldman
eadc2301e0 Fetch the unit eval commit before checking it out (#42636)
Release Notes:

- N/A
2025-11-13 15:21:53 +00:00
Richard Feldman
b500470391 Disabled agent commands (#42579)
Closes #31346

Release Notes:

- Agent commands no longer show up in the command palette when `agent`
is disabled. Same for edit predictions.
2025-11-13 10:10:02 -05:00
Oleksiy Syvokon
55e4258147 agent: Workaround for Sonnet inserting </parameter> tag (#42634)
Release Notes:

- N/A
2025-11-13 15:09:16 +00:00
Agus Zubiaga
8467a1b08b zeta eval: Improve output (#42629)
Hides the aggregated scores if only one example/repetition ran. It also
fixes an issue with the expected context scoring.

Release Notes:

- N/A
2025-11-13 14:47:48 +00:00
Tim McLean
fb90b12073 Add retry support for OpenAI-compatible LLM providers (#37891)
Automatically retry the agent's LLM completion requests when the
provider returns 429 Too Many Requests. Uses the Retry-After header to
determine the retry delay if it is available.

Many providers are frequently overloaded or have low rate limits. These
providers are essentially unusable without automatic retries.

Tested with Cerebras configured via openai_compatible.

Related: #31531 

Release Notes:

- Added automatic retries for OpenAI-compatible LLM providers

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-11-13 14:15:46 +00:00
Mayank Verma
92e64f9cf0 settings: Add tilde expansion support for LSP binary path (#41715)
Closes #38227

Release Notes:

- Added tilde expansion support for LSP binary path in `settings.json`
2025-11-13 09:14:18 -05:00
Remco Smits
f318bb5fd7 markdown: Add support for HTML href elements (#42265)
This PR adds support for `HTML` href elements. It also refactored the
way we stored the regions, this was done because otherwise I had to add
2 extra arguments to each `HTML` parser method. It's now also more
inline with how we have done it for the highlights.

**Small note**: the markdown parser only supports HTML href tags inside
a paragraph tag. So adding them as a root node will result in just
showing the inner text. This is a limitation of the markdown parser we
use itself.

**Before**
<img width="935" height="174" alt="Screenshot 2025-11-08 at 15 40 28"
src="https://github.com/user-attachments/assets/42172222-ed49-4a4b-8957-a46330e54c69"
/>

**After**
<img width="1026" height="180" alt="Screenshot 2025-11-08 at 15 29 55"
src="https://github.com/user-attachments/assets/9e139c2d-d43a-4952-8d1f-15eb92966241"
/>

**Example code**
```markdown
<p>asd <a href="https://example.com">Link Text</a> more text</p>
<p><a href="https://example.com">Link Text</a></p>

[Duck Duck Go](https://duckduckgo.com)
```

**TODO**:
- [x] Add tests

cc @bennetbo

Release Notes:

- Markdown Preview: Add support for `HTML` href elements.

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-11-13 15:12:17 +01:00
Piotr Osiewicz
430b55405a search: New recent old search implementation (#40835)
This is an in-progress work on changing how task scheduler affects
performance of project search. Instead of relying on tasks being
executed at a discretion of the task scheduler, we want to experiment
with having a set of "agents" that prioritize driving in-progress
project search matches to completion over pushing the whole thing to
completion. This should hopefully significantly improve throughput &
latency of project search.

This PR has been reverted previously in #40831.

Release Notes:
- Improved project search performance in local projects.

---------

Co-authored-by: Smit Barmase <smit@zed.dev>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-13 14:56:40 +01:00
Lukas Wirth
27f700e2b2 askpass: Quote paths in generated askpass script (#42622)
Closes https://github.com/zed-industries/zed/issues/42618

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-13 14:37:47 +01:00
Smit Barmase
b5633f5bc7 editor: Improve multi-buffer header filename click to jump to the latest selection from that buffer - take 2 (#42613)
Relands https://github.com/zed-industries/zed/pull/42480

Release Notes:

- Clicking the multi-buffer header file name or the "Open file" button
now jumps to the most recent selection in that buffer, if one exists.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-11-13 17:14:33 +05:30
R.Amogh
b9ce52dc95 agent_ui: Fix scrolling in context server configuration modal (#42502)
## Summary

Fixes #42342

When installing a dev extension with long installation instructions, the
configuration modal would overflow and users couldn't scroll to see the
full content or interact with buttons at the bottom.

## Solution

This PR adds a `ScrollHandle` to the `ConfigureContextServerModal` and
passes it to the `Modal` component, enabling the built-in modal
scrolling capability. This ensures all content remains accessible
regardless of length.

## Changes

- Added `ScrollHandle` import to the ui imports
- Added `scroll_handle: ScrollHandle` field to
`ConfigureContextServerModal` struct
- Initialize `scroll_handle` with `ScrollHandle::new()` when creating
the modal
- Pass the scroll handle to `Modal::new()` instead of `None`

## Testing

- Built the changes locally
- Tested with extensions that have long installation instructions
- Verified scrolling works and all content is accessible
- Confirmed no regression for extensions with short descriptions

Release Notes:

- Fixed scrolling issue in extension configuration modal when
installation instructions overflow the viewport

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-11-13 12:41:38 +01:00
mikeHag
34a7cfb2e5 Update cargo.rs to allow debugging of integration test annotated with the ignore attribute (#42574)
Address #40429

If an integration test is annotated with the ignore attribute, allow the
"debug: Test" option of the debug scenario or Code Action to run with
"--include-ignored"

Closes #40429

Release Notes:

- N/A
2025-11-13 12:31:23 +01:00
Kirill Bulatov
99016e3a85 Update outdated dependencies (#42611)
New rustc starts to output a few warnings, fix them by updating the
corresponding packages.

<details>
  <summary>Incompatibility notes</summary>
    
  ```
The following warnings were discovered during the build. These warnings
are an
indication that the packages contain code that will become an error in a
future release of Rust. These warnings typically cover changes to close
soundness problems, unintended or undocumented behavior, or critical
problems
that cannot be fixed in a backwards-compatible fashion, and are not
expected
to be in wide use.

Each warning should contain a link for more information on what the
warning
means and how to resolve it.


To solve this problem, you can try the following approaches:


- Some affected dependencies have newer versions available.
You may want to consider updating them to a newer version to see if the
issue has been fixed.

num-bigint-dig v0.8.4 has the following newer versions available: 0.8.5,
0.9.0, 0.9.1

- If the issue is not solved by updating the dependencies, a fix has to
be
implemented by those dependencies. You can help with that by notifying
the
maintainers of this problem (e.g. by creating a bug report) or by
proposing a
fix to the maintainers (e.g. by creating a pull request):

  - num-bigint-dig@0.8.4
  - Repository: https://github.com/dignifiedquire/num-bigint
- Detailed warning command: `cargo report future-incompatibilities --id
1 --package num-bigint-dig@0.8.4`

- If waiting for an upstream fix is not an option, you can use the
`[patch]`
section in `Cargo.toml` to use your own version of the dependency. For
more
information, see:

https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section

The package `num-bigint-dig v0.8.4` currently triggers the following
future incompatibility lints:
> warning: macro `vec` is private
> -->
/Users/someonetoignore/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-dig-0.8.4/src/biguint.rs:490:22
>     |
> 490 |         BigUint::new(vec![1])
>     |                      ^^^
>     |
> = warning: this was previously accepted by the compiler but is being
phased out; it will become a hard error in a future release!
> = note: for more information, see issue #120192
<https://github.com/rust-lang/rust/issues/120192>
> 
> warning: macro `vec` is private
> -->
/Users/someonetoignore/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-dig-0.8.4/src/biguint.rs:2005:9
>      |
> 2005 |         vec![0]
>      |         ^^^
>      |
> = warning: this was previously accepted by the compiler but is being
phased out; it will become a hard error in a future release!
> = note: for more information, see issue #120192
<https://github.com/rust-lang/rust/issues/120192>
> 
> warning: macro `vec` is private
> -->
/Users/someonetoignore/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-dig-0.8.4/src/biguint.rs:2027:16
>      |
> 2027 |         return vec![b'0'];
>      |                ^^^
>      |
> = warning: this was previously accepted by the compiler but is being
phased out; it will become a hard error in a future release!
> = note: for more information, see issue #120192
<https://github.com/rust-lang/rust/issues/120192>
> 
> warning: macro `vec` is private
> -->
/Users/someonetoignore/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-dig-0.8.4/src/biguint.rs:2313:13
>      |
> 2313 |             vec![0]
>      |             ^^^
>      |
> = warning: this was previously accepted by the compiler but is being
phased out; it will become a hard error in a future release!
> = note: for more information, see issue #120192
<https://github.com/rust-lang/rust/issues/120192>
> 
> warning: macro `vec` is private
> -->
/Users/someonetoignore/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-dig-0.8.4/src/prime.rs:138:22
>     |
> 138 |     let mut moduli = vec![BigUint::zero(); prime_limit];
>     |                      ^^^
>     |
> = warning: this was previously accepted by the compiler but is being
phased out; it will become a hard error in a future release!
> = note: for more information, see issue #120192
<https://github.com/rust-lang/rust/issues/120192>
> 
> warning: macro `vec` is private
> -->
/Users/someonetoignore/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/num-bigint-dig-0.8.4/src/bigrand.rs:319:25
>     |
> 319 |         let mut bytes = vec![0u8; bytes_len];
>     |                         ^^^
>     |
> = warning: this was previously accepted by the compiler but is being
phased out; it will become a hard error in a future release!
> = note: for more information, see issue #120192
<https://github.com/rust-lang/rust/issues/120192>
> 

  ```
  
</details>

Release Notes:

- N/A
2025-11-13 10:35:16 +00:00
Lukas Wirth
dea3c8c949 remote: More nushell fixes (#42608)
Closes https://github.com/zed-industries/zed/issues/42594

Release Notes:

- Fixed remote server installation failing with nutshell
2025-11-13 09:53:31 +00:00
Lukas Wirth
7eac6d242c diagnostics: Workaround weird panic in update_path_excerpts (#42602)
Fixes ZED-36P

Patching this over for now until I can figure out the cause of this

Release Notes:

- Fixed panic in diagnostics pane
2025-11-13 09:13:54 +00:00
Lukas Wirth
b92b28314f Replace {floor/ceil}_char_boundary polyfills with std (#42599)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-13 08:11:18 +00:00
AidanV
1fc0642de1 vim: Make each vim repeat its own transaction (#41735)
Release Notes:

- Pressing `u` after multiple `.` in rapid succession will now only undo
the latest repeat instead of all repeats.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-13 06:46:14 +00:00
Conrad Irwin
045ac6d1b6 Release failure visibility (#42572)
Closes #ISSUE

Release Notes:

- N/A
2025-11-12 23:11:09 -07:00
Sean Hagstrom
1936f16c62 editor: Use a single newline between each copied line from a multi-cursor selection (#41204)
Closes #40923

Release Notes:

- Fixed the amount of newlines between copied lines from a multi-cursor
selection of multiple full-line copies.

---


https://github.com/user-attachments/assets/ab7474d6-0e49-4c29-9700-7692cd019cef
2025-11-12 22:58:13 -07:00
Conrad Irwin
b32559f07d Avoid re-creating releases when re-running workflows (#42573)
Closes #ISSUE

Release Notes:

- N/A
2025-11-12 21:50:15 -07:00
Julia Ryan
28adedf1fa Disable env clearing for npm subcommands (#42587)
Fixes #39448

Several node version managers such as [volta](https://volta.sh) use thin
wrappers that locate the "real" node/npm binary with an env var that
points at their install root. When it finds this, it prepends the
correct directory to PATH, otherwise it'll check a hardcoded default
location and prepend that to PATH if it exists.

We were clearing env for npm subcommands, which meant that volta and co.
failed to locate the install root, and because they were installed via
scoop they don't use the default install path either so it simply
doesn't prepend anything to PATH (winget on the other hand installs
volta to the right place, which is why it worked when using that instead
of scoop to install volta @IllusionaryX).

So volta's npm wrapper executes a subcommand `npm`, but when that
doesn't prepend a different directory to PATH the first `npm` found in
PATH is that same wrapper itself, which horrifyingly causes itself to
re-exec continuously. I think they might have some logic to try to
prevent this using, you'll never guess, another env var that they set
whenever a volta wrapper execs something. Of course since we clear the
env that var also fails to propagate.

Removing env clearing (but keeping the prepending of npm path from your
settings) fixes these issues.

Release Notes:

- Fixed issues with scoop installations of mise/volta

Co-authored-by: John Tur <john-tur@outlook.com>
2025-11-12 22:03:59 -06:00
Max Brunsfeld
c9e231043a Report discarded zeta predictions and indicate whether they were shown (#42403)
Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-11-12 16:41:04 -08:00
Richard Feldman
ede3b1dae6 Allow running concurrent unit evals (#42578)
Right now only one unit eval GitHub Action can be run at a time. This
permits them to run concurrently.

Release Notes:

- N/A
2025-11-12 22:04:38 +00:00
Agus Zubiaga
b0700a4625 zeta eval: --repeat flag (#42569)
Adds a `--repeat` flag to the zeta eval that runs each example as many
times as specified. Also makes the output nicer in a few ways.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Michael <michael@zed.dev>
2025-11-12 16:58:22 -05:00
Michael Sloan
f2a1eb9963 Make check-licenses script check that AGPL crates are not included in release binaries (#42571)
See discussion in #24657. Recalled that I had a stashed change for this,
so polished it up

Release Notes:

- N/A
2025-11-12 21:58:12 +00:00
Andrew Farkas
0c1ca2a45a Improve pane: reopen closed item to not reopen closed tabs (#42568)
Closes #42134

Release Notes:

- Improved `pane: reopen closed item` to not reopen closed tabs.
2025-11-12 21:08:41 +00:00
Conrad Irwin
8fd8b989a6 Use powershell for winget job steps (#42565)
Co-Authored-By: Claude

Release Notes:

- N/A
2025-11-12 13:41:20 -07:00
Lucas Parry
fd837b348f project_panel: Make natural sort ordering consistent with other apps (#41080)
The existing sorting approach when faced with `Dir1`, `dir2`, `Dir3`,
would only get as far as comparing the stems without numbers (`dir` and
`Dir`), and then the lowercase-first tie breaker in that function would
determine that `dir2` should come first, resulting in an undesirable
order of `dir2`, `Dir1`, `Dir3`.

This patch defers tie-breaking until it's determined that there's no
other difference in the strings outside of case to order on, at which
point we tie-break to provide a stable sort.

Natural number sorting is still preserved, and mixing different cases
alphabetically (as opposed to all lowercase alpha, followed by all
uppercase alpha) is preserved.

Closes #41080


Release Notes:

- Fixed: ProjectPanel sorting bug

Screenshots:

Before | After
----|---
<img width="237" height="325" alt="image"
src="https://github.com/user-attachments/assets/6e92e8c0-2172-4a8f-a058-484749da047b"
/> | <img width="239" height="325" alt="image"
src="https://github.com/user-attachments/assets/874ad29f-7238-4bfc-b89b-fd64f9b8889a"
/>

I'm having trouble reasoning through what was previously going wrong
with `docs` in the before screenshot, but it also seems to now appear
alphabetically where you'd expect it with this patch

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-13 02:04:40 +05:30
Piotr Osiewicz
6b239c3a9a Bump Rust to 1.91.1 (#42561)
Release Notes:

- N/A

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-11-12 20:27:04 +00:00
Piotr Osiewicz
73e5df6445 ci: Install pre-built cargo nextest instead of rolling our own (#42556)
Closes #ISSUE

Release Notes:

- N/A
2025-11-12 20:05:40 +00:00
KyleBarton
b403c199df Add additional comment for context in Tyepscript highlights (#42564)
This adds additional comments which were left out from #42494 by
accident. Namely, it describes why we have additional custom
highlighting in `highlights.scm` for the Typescript grammar.

Release Notes:

- N/A
2025-11-12 19:59:10 +00:00
Konstantinos Lyrakis
cb4067723b Fix typo (#42559)
Fixed a typo in the docs

Release Notes:

- N/A
2025-11-12 21:07:34 +02:00
Ben Kunkle
1c625f8783 Fix JSON Schema documentation for code_actions_on_format (#42128)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-12 18:33:02 +00:00
KyleBarton
4adec27a3d Implement pretty TypeScript errors (#42494)
Closes #7844

This change uses tree-sitter highlights as a method of showing
typescript errors prettily, keeping regex as simple as possible:

<img width="832" height="446" alt="Screenshot 2025-11-11 at 3 40 24 PM"
src="https://github.com/user-attachments/assets/0b3b6cf1-4d4d-4398-b89b-ef5ec0df87ec"
/>

It covers three main areas:

1. Diagnostics

Diagnostics are now rendered with language-aware typescript, by
providing the project's language registry.

2. Vtsls

The LSP provider for typescript now implements the
`diagnostic_message_to_markdown` function in the `LspAdapter` trait, so
as to provide Diagnostics with \`\`\`typescript...\`\`\`-style code
blocks for any selection of typescript longer than one word. In the
single-word case, it simply wraps with \`\`

3. Typescript's `highlights.scm`

`vtsls` doesn't provide strictly valid typescript in much of its
messaging. Rather, it returns a message with snippets of typescript
values which are invalid. Tree-sitter was not properly highlighting
these snippets because it was expecting key-value formats. For instance:
```
type foo = { foo: string; bar: string; baz: number[] }
```
is valid, whereas simply
```
{ foo: string; bar: string; baz: number[] }
```
is not.

Therefore, highlights.scm needed to be adjusted in order to
pattern-match on literal values that might be returned from the vtsls
diagnostics messages. This was done by a) identifying arrow functions on
their own, and b) augmenting the `statment_block` pattern matching in
order to match on values which were clearly object literals.

This approach may not be exhaustive - I'm happy to work on any
additional cases we might identify from `vtsls` here - but hopefully
demonstrates an extensible approach to making these messages look nice,
without taking on the technical burden of extensive regex.

Release Notes:

- Show pretty TypeScript errors with language-aware Markdown.
2025-11-12 10:32:46 -08:00
Remco Smits
e8daab15ab debugger: Fix prevent creating breakpoints inside breakpoint editor (#42475)
Closes #38057

This PR fixes that you can no longer create breakpoints inside the
breakpoint editor in code called `BreakpointPromptEditor`. As you can
see, inside the after video, there is no breakpoint editor created
anymore.

**Before**


https://github.com/user-attachments/assets/c4e02684-ac40-4176-bd19-f8f08e831dde

**After**


https://github.com/user-attachments/assets/f5b1176f-9545-4629-be12-05c64697a3de

Release Notes:

- Debugger: Prevent breakpoints from being created inside the breakpoint
editor
2025-11-12 18:18:10 +00:00
Ben Kunkle
6501b0c311 zeta eval: Improve determinism and debugging ergonomics (#42478)
- Improves the determinism of the search step for better cache
reusability
- Adds a `--cache force` mode that refuses to make any requests or
searches that aren't cached
- The structure of the `zeta-*` directories under `target` has been
rethought for convenience

Release Notes:

- N/A

---------

Co-authored-by: Agus <agus@zed.dev>
2025-11-12 18:16:13 +00:00
Ben Kunkle
6c0069ca98 zeta2: Improve error reporting and eval purity (#42470)
Closes #ISSUE

Improves error reporting for various failure modes of zeta2, including
failing to parse the `<old_text>`/`<new_text>` pattern, and the contents
of `<old_text>` failing to match.

Additionally, makes it so that evals are checked out into a worktree
with the _repo_ name instead of the _example_ name, in order to make
sure that the eval name has no influence on the models prediction. The
repo name worktrees are still namespaced by the example name like
`{example_name}/{repo_name}` to ensure evals pointing to the same repo
do not conflict.

Release Notes:

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

---------

Co-authored-by: Agus <agus@zed.dev>
2025-11-12 12:52:11 -05:00
Conrad Irwin
c8930e07a3 Allow multiple parked threads in tests (#42551)
Closes #ISSUE

Release Notes:

- N/A

Co-Authored-By: Piotr <piotr@zed.dev>
2025-11-12 10:29:31 -07:00
Richard Feldman
ab352f669e Gracefully handle @mention-ing large files with no outlines (#42543)
Closes #32098

Release Notes:

- In the Agent panel, when `@mention`-ing large files with no outline,
their first 1KB is now added to context
2025-11-12 16:55:25 +00:00
Finn Evers
e79188261b fs: Fix wrong watcher trace log on Linux (#42544)
Follow-up to #40200

Release Notes:

- N/A
2025-11-12 16:26:53 +00:00
Marshall Bowers
ab62739605 collab: Remove unused methods from User model (#42536)
This PR removes some unused methods from the `User` model.

Release Notes:

- N/A
2025-11-12 15:38:16 +00:00
Marco Mihai Condrache
cfbde91833 terminal: Add setting for scroll multiplier (#39463)
Closes #5130

Release Notes:

- Added setting option for scroll multiplier of the terminal

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
Co-authored-by: MrSubidubi <finn@zed.dev>
2025-11-12 16:38:06 +01:00
Vasyl Protsiv
80b32ddaad gpui: Add 'Nearest' scrolling strategy to 'UniformList' (#41844)
This PR introduces `Nearest` scrolling strategy to `UniformList`. This
is now used in completions menu and the picker to choose the appropriate
scrolling strategy depending on movement direction. Previously,
selecting the next element after the last visible item caused the menu
to scroll with `ScrollStrategy::Top`, which scrolled the whole page and
placed the next element at the top. This behavior is inconsistent,
because using `ScrollStrategy::Top` when moving up only scrolls one
element, not the whole page.


https://github.com/user-attachments/assets/ccfb238f-8f76-4a18-a18d-bbcb63340c5a

The solution is to introduce the `Nearest` scrolling strategy which will
internally choose the scrolling strategy depending on whether the new
selected item is below or above currently visible items. This ensures a
single-item scroll regardless of movement direction.


https://github.com/user-attachments/assets/8502efb8-e2c0-4ab1-bd8d-93103841a9c4


I also noticed that some functions in the file have different logic
depending on `y_flipped`. This appears related to reversing the order of
elements in the list when the completion menu appears above the cursor.
This was a feature suggested in #11200 and implemented in #23446. It
looks like this feature was reverted in #27765 and there currently seem
to be no way to have `y_flipped` to be set to `true`.

My understanding is that the opposite scroll strategy should be used if
`y_flipped`, but since there is no way to enable this feature to test it
and I don't know if the feature is ever going to be reintroduced I
decided not to include it in this PR.


Release Notes:

- gpui: Add 'Nearest' scrolling strategy to 'UniformList'
2025-11-12 16:37:14 +01:00
Joseph T. Lyons
53652cdb3f Bump Zed to v0.214 (#42539)
Release Notes:

- N/A
2025-11-12 15:36:28 +00:00
Smit Barmase
1d75a9c4b2 Reverts "add OpenExcerptsSplit and dispatches on click" (#42538)
Partially reverts https://github.com/zed-industries/zed/pull/42283 to
restore the old behavior of excerpt clicking.

Release Notes:

- N/A
2025-11-12 20:47:29 +05:30
Richard Feldman
c5ab1d4679 Stop thread on Restore Checkpoint (#42537)
Closes #35142

In addition to cleaning up the terminals, also stops the conversation.

Release Notes:

- Restoring a checkpoint now stops the agent conversation.
2025-11-12 15:13:40 +00:00
Smit Barmase
1fdd95a9b3 Revert "editor: Improve multi-buffer header filename click to jump to the latest selection from that buffer" (#42534)
Reverts zed-industries/zed#42480

This panics on Nightly in cases where anchor might not be valid for that
snapshot. Taking it back before the cutoff.

Release Notes:

- N/A
2025-11-12 20:31:43 +05:30
localcc
49634f6041 Miniprofiler (#42385)
Release Notes:

- Added hang detection and a built in performance profiler
2025-11-12 15:31:20 +01:00
Jakub Konka
2119ac42d7 git_panel: Fix partially staged changes not showing up (#42530)
Release Notes:

- N/A
2025-11-12 15:13:29 +01:00
Hans
e833d1af8d vim: Fix change surround adding unwanted spaces with quotes (#42431)
Update `Vim.change_surround` in order to ensure that there's no
overlapping edits by keeping track of where the open string range ends
and ensuring that the closing string range start does not go lower than
the open string range end.

Closes #42316 

Release Notes:

- Fix vim's change surrounds `cs` inserting spaces with quotes by
preventing overlapping edits

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-11-12 13:04:24 +00:00
Ben Kunkle
7be76c74d6 Use set -x in script/clear-target-dir-if-larger-than (#42525)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-12 12:52:19 +00:00
Piotr Osiewicz
c2980cba18 remote_server: Bump fork to 0.4.0 (#42520)
Release Notes:

- N/A
2025-11-12 11:57:53 +00:00
Lena
a0be53a190 Wake up stalebot with an updated config (#42516)
- switch the bot from looking at the `bug/crash` labels which we don't
  use anymore to the Bug/Crash issue types which we do use
- shorten the period of time after which a bug is suspected to be stale
  (with our pace they can indeed be outdated in 60 days)
- extend the grace period for someone to come around and say nope, this
  problem still exists (people might be away for a couple of weeks).


Release Notes:

- N/A
2025-11-12 12:40:26 +01:00
Lena
70feff3c7a Add a one-off cleanup script for GH issue types (#42515)
Mainly for historical purposes and in case we want to do something similar enough in the future.

Release Notes:

- N/A
2025-11-12 11:40:31 +01:00
Finn Evers
f46990bac8 extensions_ui: Add XML extension suggestion for XML files (#42514)
Closes #41798

Release Notes:

- N/A
2025-11-12 10:12:02 +00:00
Lukas Wirth
78f466559a vim: Fix empty selections panic in insert_at_previous (#42504)
Fixes ZED-15C

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-12 09:54:22 +00:00
CnsMaple
4f158c1983 docs: Update basedpyright settings examples (#42497)
The
[example](https://docs.basedpyright.com/latest/configuration/language-server-settings/#zed)
on the official website of basedpyright is correct.

Release Notes:

- Update basedpyright settings examples
2025-11-12 10:05:17 +01:00
Kirill Bulatov
ddf762e368 Revert "gpui: Unify the index_for_x methods (#42162)" (#42505)
This reverts commit 082b80ec89.

This broke clicking, e.g. in snippets like

```rs
let x = vec![
    1, 2, //
    3,
];
```

clicking between `2` and `,` is quite off now.

Release Notes:

- N/A
2025-11-12 08:24:06 +00:00
Lukas Wirth
f2cadad49a gpui: Fix RefCell already borrowed in WindowsPlatform::run (#42506)
Relands #42440 

Fixes ZED-1VX

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-12 08:19:32 +00:00
Lukas Wirth
231d1b1d58 diagnostics: Close diagnosticsless buffers on refresh (#42503)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-12 08:11:50 +00:00
Andrew Farkas
2bcfc12951 Absolutize LSP and DAP paths more conservatively (#42482)
Fixes a regression caused by #42135 where LSP and DAP binaries weren't
being used from `PATH` env var

Now we absolutize the path if (path is relative AND (path has multiple
components OR path exists in worktree)).

- Relative paths with multiple components might not exist in the
worktree because they are ignored. Paths with a single component will at
least have an entry saying that they exist and are ignored.
- Relative paths with multiple components will never use the `PATH` env
var, so they can be safely absolutized

Release Notes:

- N/A
2025-11-12 01:36:22 +00:00
Richard Feldman
cf6ae01d07 Show recommended models under normal category too (#42489)
<img width="395" height="444" alt="Screenshot 2025-11-11 at 4 04 57 PM"
src="https://github.com/user-attachments/assets/8da68721-6e33-4d01-810d-4aa1e2f3402d"
/>

Discussed with @danilo-leal and we're going with the "it's checked in
both places" design!

Closes #40910

Release Notes:

- Recommended AI models now still appear in their normal category in
addition to "Recommended:"
2025-11-11 22:10:46 +00:00
Miguel Cárdenas
2ad7ecbcf0 project_panel: Add auto_open settings (#40435)
- Based on #40234, and improvement of #40331

Release Notes:

- Added granular settings to control when files auto-open in the project
panel (project_panel.auto_open.on_create, on_paste, on_drop)

<img width="662" height="367" alt="Screenshot_2025-10-16_17-28-31"
src="https://github.com/user-attachments/assets/930a0a50-fc89-4c5d-8d05-b1fa2279de8b"
/>

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-12 03:23:40 +05:30
Lukas Wirth
854c6873c7 Revert "gpui: Fix RefCell already borrowed in WindowsPlatform::run" (#42481)
Reverts zed-industries/zed#42440

There are invalid temporaries in here keeping the borrows alive for
longer
2025-11-11 21:42:59 +00:00
Andrew Farkas
da94f898e6 Add support for multi-word snippet prefixes (#42398)
Supercedes #41126

Closes #39559, #35397, and #41426

Release Notes:

- Added support for multi-word snippet prefixes

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-11-11 16:34:25 -05:00
Richard Feldman
f62bfe1dfa Use enterprise_uri for settings when provided (#42485)
Closes #34945

Release Notes:

- Fixed `enterprise_uri` not being used for GitHub settings URL when
provided
2025-11-11 21:31:42 +00:00
Richard Feldman
a56693d9e8 Fix panic when opening an invalid URL (#42483)
Now instead of a panic we see this:

<img width="511" height="132" alt="Screenshot 2025-11-11 at 3 47 25 PM"
src="https://github.com/user-attachments/assets/48ba2f41-c5c0-4030-9331-0d3acfbf9461"
/>


Release Notes:

- Trying to open invalid URLs in a browser now shows an error instead of
panicking
2025-11-11 21:24:37 +00:00
Smit Barmase
b4b7a23c39 editor: Improve multi-buffer header filename click to jump to the latest selection from that buffer (#42480)
Closes https://github.com/zed-industries/zed/pull/42099

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

Release Notes:

- Clicking the multi-buffer header file name or the "Open file" button
now jumps to the most recent selection in that buffer, if one exists.
2025-11-12 02:04:37 +05:30
Richard Feldman
0d56ed7d91 Only send unit eval failures to Slack for cron job (#42479)
Release Notes:

- N/A
2025-11-11 20:19:34 +00:00
Lay Sheth
e01e0b83c4 Avoid panics in LSP store path handling (#42117)
Release Notes:

- Fixed incorrect journal paths handling
2025-11-11 20:51:57 +02:00
Richard Feldman
908ef03502 Split out cron and non-cron unit evals (#42472)
Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-11-11 13:45:48 -05:00
feeiyu
5f4d0dbaab Fix circular reference issue around PopoverMenu (#42461)
Follow up to https://github.com/zed-industries/zed/pull/42351

Release Notes:

- N/A
2025-11-11 19:20:38 +02:00
brequet
c50f821613 docs: Fix typo in configuring-zed.md (#42454)
Fix a minor typo in the setting key: `auto_install_extension` should be
`auto_install_extensions`.

Release Notes:

- N/A
2025-11-11 17:58:18 +01:00
Marshall Bowers
7e491ac500 collab: Drop embeddings table (#42466)
This PR drops the `embeddings` table, as it is no longer used.

Release Notes:

- N/A
2025-11-11 11:44:04 -05:00
Richard Feldman
9e1e732db8 Use longer timeout on evals (#42465)
The GPT-5 ones in particular can take a long time!

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-11-11 16:37:20 +00:00
Lukas Wirth
83351283e4 settings: Skip terminal env vars with substitutions in vscode import (#42464)
Closes https://github.com/zed-industries/zed/issues/40547

Release Notes:

- Fixed vscode import creating faulty terminal env vars in terminal
settings
2025-11-11 16:15:12 +00:00
Marshall Bowers
03acbb7de3 collab: Remove unused embeddings queries and model (#42463)
This PR removes the queries and database model for embeddings, as
they're no longer used.

Release Notes:

- N/A
2025-11-11 16:13:59 +00:00
Richard Feldman
0268b17096 Add more secrets to eval workflows (#42459)
Release Notes:

- N/A
2025-11-11 16:07:57 +00:00
Danilo Leal
993919d360 agent_ui: Add icon button to trigger the @-mention completions menu (#42449)
Closes https://github.com/zed-industries/zed/issues/37087

This PR adds an icon button to the footer of the message editor enabling
to trigger and interact with the @-mention completions menu with the
mouse. This is a first step towards making other types of context you
can add in Zed's agent panel more discoverable. Next, I want to improve
the discoverability of images and selections, given that you wouldn't
necessarily know they work in Zed without a clear way to see them. But I
think that for now, this is enough to close the issue above, which had
lots of productive comments and discussion!

<img width="500" height="540" alt="Screenshot 2025-11-11 at 10  46 3@2x"
src="https://github.com/user-attachments/assets/fd028442-6f77-4153-bea1-c0b815da4ac6"
/>

Release Notes:

- agent: Added an icon button in the agent panel that allows to trigger
the @-mention menu (for adding context) now also with the mouse.
2025-11-11 12:50:56 -03:00
Danilo Leal
8467a3dbd6 agent_ui: Allow to uninstall agent servers from the settings view (#42445)
This PR also adds items within the "Add Agent" menu to:
1. Add more agent servers from extensions, opening up the extensions
page with "Agent Servers" already filtered
2. Go to the agent server + ACP docs to learn more about them

I feel like having them there is a nice way to promote this knowledge
from within the product and have users learn more about them.

<img width="500" height="540" alt="Screenshot 2025-11-11 at 10  46 3@2x"
src="https://github.com/user-attachments/assets/9449df2e-1568-44d8-83ca-87cbb9eefdd2"
/>

Release Notes:

- agent: Enabled uninstalled agent servers from the agent panel's
settings view.
2025-11-11 12:47:08 -03:00
Bennet Bo Fenner
ee2e690657 agent_servers: Fix panic when setting default mode (#42452)
Closes ZED-35A

Release Notes:

- Fixed an issue where Zed would panic when trying to set the default
mode for ACP agents
2025-11-11 15:25:27 +00:00
tidely
28d019be2e ollama: Fix tool calling (#42275)
Closes #42303

Ollama added tool call identifiers
(https://github.com/ollama/ollama/pull/12956) in its latest version
[v0.12.10](https://github.com/ollama/ollama/releases/tag/v0.12.10). This
broke our json schema and made all tool calls fail.

This PR fixes the schema and uses the Ollama provided tool call
identifier when available. We remain backwards compatible and still use
our own identifier with older versions of Ollama. I added a `TODO` to
remove the `Option` around the new field when most users have updated
their installations to v0.12.10 or above.

Note to reviewer: The fix to this issue should likely get cherry-picked
into the next release, since Ollama becomes unusable as an agent without
it.

Release Notes:

- Fixed tool calling when using the latest version of Ollama
2025-11-11 16:10:47 +01:00
Lukas Wirth
a19d11184d remote: Add more context to error logging in wsl (#42450)
cc https://github.com/zed-industries/zed/issues/40892

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-11 15:09:56 +00:00
Lukas Wirth
38e2c7aa66 editor: Hide file blame on editor cancel (ESC) (#42436)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-11 13:56:04 +00:00
liuyanghejerry
10d5d78ded Improve error messages on extension loading (#42266)
This pull request improves error message when extension loading goes
wrong.

Before:

```
2025-11-08T21:16:02+08:00 ERROR [extension_host::extension_host] failed to load arkts extension.toml

Caused by:
    No such file or directory (os error 2)
```

Now:

```
2025-11-08T22:57:00+08:00 ERROR [extension_host::extension_host] failed to load arkts extension.toml, "/Users/user_name_placeholder/Library/Application Support/Zed/extensions/installed/arkts/extension.toml"

Caused by:
    No such file or directory (os error 2)

```

Release Notes:

- N/A
2025-11-11 15:45:03 +02:00
Terra
dfd7e85d5d Replace deprecated json.schemastore.org with www.schemastore.org (#42336)
Release Notes:

- N/A

According to
[microsoft/vscode#254689](https://github.com/microsoft/vscode/issues/254689),
the json.schemastore.org domain has been deprecated and should now use
www.schemastore.org (or schemastore.org) instead.

This PR updates all occurrences of the old domain within the Zed
codebase,
including code, documentation, and configuration files.
2025-11-11 15:43:25 +02:00
Lukas Wirth
b8fcd3ea04 gpui: Fix RefCell already borrowed in WindowsPlatform::run (#42440)
Fixes ZED-1VX

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-11 13:43:06 +00:00
Libon
9be5e31aca Add clear recent files history command (#42176)
![2025-11-07
181619](https://github.com/user-attachments/assets/a9bef7a6-dc0b-4db2-85e5-2e1df7b21cfa)


Release Notes:

- Added "workspace: clear navigation history" command
2025-11-11 15:42:00 +02:00
Kirill Bulatov
58db38722b Find proper applicable chunks for visible ranges (#42422)
Release Notes:

- Fixed inlay hints not being queried for certain long-ranged jumps

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-11 13:38:28 +00:00
Agus Zubiaga
f2ad0d716f zeta cli: Print log paths when running predict (#42396)
Release Notes:

- N/A

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-11 09:56:20 -03:00
Lukas Wirth
777b46533f auto_update: Ignore dir removal errors on windows (#42435)
The auto update helper already removes these when successful, so these
will always fail in the common case.

Additional replaces a mutable const with a static as otherwise we'll
rebuild the job list on every access

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-11 12:55:19 +00:00
Miguel Raz Guzmán Macedo
b3dd51560b docs: Fix broken links in docs with lychee (#42404)
Lychee is a [Rust based](https://lychee.cli.rs) async parallel link
checker.

I ran it against the codebase to suss out stale links and fixed those
up.

There's currently 2 remaining cases that I don't know how to resolve:

1. https://flathub.org/apps/dev.zed.Zed - nginx is giving a 502 bad
gateway
2.
https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg
- I don't want to mess with the CI pipeline in this PR.

Once again, I'll punt to the Docs Czar to see if this gets incorporated
into CI later.

---

## Running `lychee` locally:

```
cargo binstall -y lychee
lychee .
```

---
Release Notes:

- N/A

Signed-off-by: mrg <miguelraz@ciencias.unam.mx>
2025-11-11 13:55:02 +01:00
dDostalker
25489c2b7a Fix adding a Python virtual environment, may duplicate the "open this dictionary" string when modifying content. (#41840)
Release Notes:

- Fixed an issue when adding a Python virtual environment that may cause
duplicate "open this dictionary" entries

- Trigger condition:
Type `C:\`, delete `\`, then repeatedly add `\`.

-Video

bug:

https://github.com/user-attachments/assets/f68008bb-9138-4451-a842-25b58574493b

fix:

https://github.com/user-attachments/assets/2913b8c2-adee-4275-af7e-e055fd78915f
2025-11-11 13:22:32 +01:00
Alexandre Anício
dc372e8a84 editor: Unfold buffers with selections on edit + Remove selections on buffer fold (#37953)
Closes #36376 

Problem:
Multi-cursor edits/selections in multi-buffers view were jumping to
incorrect locations after toggling buffer folds. When users created
multiple selections across different buffers in a multi-buffer view
(like project search results) and then folded one of the buffers,
subsequent text insertion would either:

1. Insert text at wrong locations (like at the top of the first unfolded
buffer)
2. Replace the entire content in some buffers instead of inserting at
the intended cursor positions
3. Create orphaned selections that caused corruption in the editing
experience

The issue seems to happen because when a buffer gets folded in a
multi-buffer view, the existing selections associated with that buffer
become invalid anchor points.

Solution:
1. Selection Cleanup on Buffer Folding
- Added `remove_selections_from_buffer()` method that filters out all
selections from a buffer when it gets folded
- This prevents invalid selections from corrupting subsequent editing
operations
- Includes edge case handling: if all selections are removed (all
buffers folded), it creates a default selection at the start of the
first buffer to prevent panics

2. Unfolding buffers before editing  
- Added `unfold_buffers_with_selections()` call in `handle_input()`
ensures buffers with active selections are automatically unfolded before
editing
- This helps in fixing an edge case (covered in the tests) where, if you
fold all buffers in a multi-buffer view, and try to insert text in a
selection, it gets unfolded before the edit happens. Without this, the
inserted text would override the entire buffer content.
- If we don't care about this edge case, we could remove this method. I
find it ok to add since we already trigger buffer unfolding after edits
with `Event::ExcerptsEdited`.

Release Notes:

- Fixed multi-cursor edits jumping to incorrect locations after toggling
buffer folds in multi-buffer views (e.g, project search)
- Multi-cursor selections now properly handle buffer folding/unfolding
operations
- Text insertion no longer occurs at the wrong positions when buffers
are folded during multi-cursor editing
- Eliminated content replacement bugs where entire buffer contents were
incorrectly overwritten
- Added safe fallback behavior when all buffers in a multi-buffer view
are folded

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-11 16:59:44 +05:30
Lukas Wirth
1c4bb60209 gpui: Fix invalid unwrap in windows window creation (#42426)
Fixes ZED-34M

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-11 10:55:19 +00:00
Dino
97100ce52f editor: Respect search case sensitivity when selecting occurrences (#42121)
Update how the editor's `select_*` methods work in order to respect the
`search.case_sensitive` setting, or to be overriden by the
`BufferSearchBar` search options.

- Update both the `SearchableItem` and `SearchableItemHandle` traits
  with a new `set_search_is_case_sensitive` method that allows callers
  to set the case sensitivity of the search
- Update the `BufferSearchBar` to leverage
  `SearchableItemHandle.set_search_is_case_sensitive` in order to sync
  its case sensitivity options with the searchable item
- Update the implementation of the `SearchableItem` trait for `Editor`
  so as to store the argument provided to the
  `set_search_is_case_sensitive` method
- Update the way search queries are built by `Editor` so as to rely on
  `SearchableItem.set_search_is_case_sensitive` argument, if not `None`,
  or default to the editor's `search.case_sensitive` settings

Closes #41070 

Release Notes:

- Improved the "Select Next Occurrence", "Select Previous Occurrence"
and "Select All Occurrences" actions in order to respect the case
sensitivity search settings

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-11 10:26:40 +00:00
Dino
dcf56144b5 vim: Sort whole buffer when no range is specified (#42376)
- Introduce a `default_range` field to `VimCommand`, to be optionally
  used when no range is specified for the command
- Update `VimCommand.parse` to take into consideration the
  `default_range`
- Introduce `CommandRange::buffer` to obtain the `CommandRange` which
  corresponds to the whole buffer
- Update the `VimCommand` definitions for both `sort` and `sort i` to
  default to the whole buffer when no range is specified

Closes #41750 

Release Notes:

- Improved vim's `:sort` command to sort the buffer's content when no
selection is used
2025-11-11 10:04:30 +00:00
Lukas Wirth
46db753f79 diagnostics: Fix panic due non-sorted diagnostics excerpt ranges (#42416)
Fixes ZED-356

Release Notes:

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

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-11 09:57:11 +01:00
Lukas Wirth
1a807a7a6a terminal: Spawn terminal process on main thread on macos again (#42411)
Closes https://github.com/zed-industries/zed/issues/42365, follow up to
https://github.com/zed-industries/zed/pull/42234

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-11 07:57:30 +00:00
Alvaro Parker
f90d0789fb git: Add notification to git clone (#41712)
Adds a simple notification when cloning a repo using the integrated git
clone on Zed. Before this, the user had no feedback after starting the
cloning action.

Demo:


https://github.com/user-attachments/assets/72fcdf1b-fc99-4fe5-8db2-7c30b170f12f

Not sure about that icon I'm using for the animation, but that can be
easily changed.

Release Notes:

- Added notification when cloning a repo from zed
2025-11-11 01:02:13 -05:00
Conrad Irwin
9e717c7711 Use cloud for auto-update (#42246)
We've had several outages with a proximate cause of "vercel is
complicated",
and auto-update is considered a critical feature; so lets not use vercel
for
that.

Release Notes:

- Auto Updates (and remote server binaries) are now downloaded via
https://cloud.zed.dev instead of https://zed.dev. As before, these URLs
redirect to the GitHub release for actual downloads.
2025-11-10 23:00:55 -07:00
CnsMaple
823844ef18 vim: Fix increment order (#42256)
before:


https://github.com/user-attachments/assets/d490573c-4c2b-4645-a685-d683f06c611f


after:


https://github.com/user-attachments/assets/a69067a1-6e68-4f05-ba56-18eadb1c54df

Release Notes:

- Fix vim increment order
2025-11-10 21:48:27 -07:00
Conrad Irwin
70bcf93355 Add an event_source to events (#42125)
Release Notes:

- N/A
2025-11-10 21:32:09 -07:00
Conrad Irwin
378b30eba5 Use cloud.zed.dev for install.sh (#42399)
Similar to #42246, we'd like to avoid having Vercel on the critical
path.

https://zed.dev/install.sh is served from Cloudflare by intercepting a
route on that page, so this makes the shell-based install flow vercel independent.

Release Notes:

- `./script/install.sh` will now fetch assets via
`https://cloud.zed.dev/`
instead of `https://zed.dev`. As before it will redirect to GitHub
releases
  to complete the download.
2025-11-10 23:55:19 +00:00
Marshall Bowers
83e7c21b2c collab: Remove unused user queries (#42400)
This PR removes queries on users that were no longer being used.

Release Notes:

- N/A
2025-11-10 23:47:39 +00:00
Finn Evers
e488b6cd0b agent_ui: Fix issue where MCP extension could not be uninstalled (#42384)
Closes https://github.com/zed-industries/zed/issues/42312

The issue here was that we assumed that context servers provided by
extensions would always need a config in the settings to be present when
actually the opposite was the case - context servers provided by
extensions are the only context servers that do not need a config to be
in place in order to be available in the UI.

Release Notes:

- Fixed an issue where context servers provided by extensions could not
be uninstalled if they were previously unconfigured.
2025-11-11 00:25:27 +01:00
Kirill Bulatov
f52549c1c4 Small documentation fixes (#42397)
Release Notes:

- N/A

Co-authored-by: Ole Jørgen Brønner <olejorgenb@gmail.com>
2025-11-11 01:16:28 +02:00
Conrad Irwin
359521e91d Allow passing model_name to evals (#42395)
Release Notes:

- N/A
2025-11-10 23:00:52 +00:00
Max Brunsfeld
b607077c08 Add old_text/new_text as a zeta2 prompt format (#42171)
Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-11-10 15:44:54 -07:00
Marshall Bowers
e5fce424b3 Update CI badge in README (#42394)
This PR updates the CI badge in the README, after the CI workflow
reorganization.

Release Notes:

- N/A
2025-11-10 22:05:46 +00:00
Andrew Farkas
a8b04369ae Refactor completions (#42122)
This is progress toward multi-word snippets (including snippets with
prefixes containing symbols)

Release Notes:

- Removed `trigger` argument in `ShowCompletions` command

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-10 17:00:59 -05:00
Marshall Bowers
11b38db3e3 collab: Drop channel_messages table and its dependents (#42392)
This PR drops the `channel_messages` table and its
dependents—`channel_message_mentions` and `observed_channel_messages`—as
they are no longer used.

Release Notes:

- N/A
2025-11-10 21:59:05 +00:00
John Tur
112b5c16b7 Add QuitMode policy to GPUI (#42391)
Applications can select a policy for when the app quits using the new
function `Application::with_quit_mode`:
- Only on explicit calls to `App::quit`
- When the last window is closed
- Platform default (former on macOS, latter everywhere else) 

Release Notes:

- N/A
2025-11-10 16:45:43 -05:00
Marshall Bowers
32ec1037e1 collab: Remove unused models left over from chat (#42390)
This PR removes some database models that were left over from the chat
feature.

Release Notes:

- N/A
2025-11-10 21:39:44 +00:00
Connor Tsui
a44fc9a1de Rename ThemeMode to ThemeAppearanceMode (#42279)
There was a TODO in `crates/settings/src/settings_content/theme.rs` to
make this rename.

This PR is just splitting off this change from
https://github.com/zed-industries/zed/pull/40035 to make reviewing that
one a bit easier since that PR is a bit more involved than expected.

Release Notes:

- N/A

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
2025-11-10 14:26:01 -07:00
Ole Jørgen Brønner
efcd7f7d10 Slightly improve completion in settings.json (for lsp.<language-server>.) (#42263)
Document "any-typed" (`serde_json::Value`) "lsp" keys to include them in
json-language-server completions.

The vscode-json-languageserver seems to skip generically typed keys when
offering completion.

For this schema

```
    "LspSettings": {
        "type": "object",
        "properties": {
            ...
            "initialization_options": true,
            ...
         }
     }
```

"initialization_options" is not offered in the completion.

The effect is easy to verify by triggering completion inside:

```
    "lsp": {
        "basedpyright": {
           COMPLETE HERE
```

<img width="797" height="215" alt="image"
src="https://github.com/user-attachments/assets/d1d1391c-d02c-4028-9888-8869f4d18b0f"
/>

By adding a documentation string the keys are offered even if they are
generically typed:

<img width="809" height="238" alt="image"
src="https://github.com/user-attachments/assets/9a072da9-961b-4e15-9aec-3d56933cbe67"
/>

---

Note: I did some cursory research of whether it's possible to make
vscode-json-languageserver change behavior without success. IMO, not
offering completions here is a bug (or at minimal should be
configurable)

---

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-11-10 23:08:40 +02:00
John Tur
aaf2f9d309 Ignore "Option as Meta" setting outside of macOS (#42367)
The "Option" key only exists on a Mac. On other operating systems, it is
always expected that the Alt key generates escaped characters.

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

Release Notes:

- N/A
2025-11-10 15:11:18 -05:00
Finn Evers
62e3a49212 editor: Fix rare panic in wrap map (#39379)
Closes ZED-1SV
Closes ZED-TG
Closes ZED-22G
Closes ZED-22J

This seems to fix the reported error there, but ultimately, this might
benefit from a test to reproduce. Hence, marking as draft for now.

Release Notes:

- Fixed a rare panic whilst wrapping lines.
2025-11-10 20:08:48 +00:00
Finn Evers
87d0401e64 editor: Show relative line numbers for deleted rows (#42378)
Closes #42191

This PR adds support for relative line numbers in deleted hunks. Note
that this only applies in cases where there is a form of relative
numbering.

It also adds some tests for this functionality as well as missing tests
for other cases in line layouting that was previously untested.

Release Notes:

- Line numbers will now be shown in deleted git hunks if relative line
numbering is enabled
2025-11-10 21:00:50 +01:00
Danilo Leal
2c375e2e0a agent_ui: Ensure message editor placeholder text is accurate (#42375)
This PR creates a dedicated function for the agent panel message
editor's placeholder text so that we can wait for the agent
initialization to capture whether they support slash commands or not. On
the one (nice) hand, this allow us to stop matching agents by name and
make this a bit more generic. On the other (bad) hand, the "/ for
commands" bit should take a little second to show up because we can only
know whether an agent supports it after it is initialized.

This is particularly relevant now that we have agents coming from
extensions and for them, we would obviously not be able to match by
name.

Release Notes:

- agent: Fixed agent panel message editor's placeholder text by making
it more accurate as to whether agents support slash commands,
particularly those coming from extensions.
2025-11-10 16:50:52 -03:00
Conrad Irwin
c24f9e47b4 Try to download wasi-sdk ahead of time (#42377)
This hopefully resolves the lingering test failures on linux,
but also adds some logging just in case this isn't the problem...

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-10 19:50:43 +00:00
Andrew Farkas
3fbfea491d Support relative paths in LSP & DAP binaries (#42135)
Closes #41214

Release Notes:

- Added support for relative paths in LSP and DAP binaries

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-11-10 19:33:00 +00:00
Finn Evers
2b369d7532 rust: Explicitly capture lifetime identifier (#42372)
Closes #42030

This matches what VSCode and basically also this capture does. However,
the identifier capture was overridden by other captures, hence the need
to be explicit here.

| Before | After | 
| - | - |
| <img width="930" height="346" alt="Bildschirmfoto 2025-11-10 um 17 56
28"
src="https://github.com/user-attachments/assets/e938c863-0981-4368-ab0a-a01dd04cfb24"
/> | <img width="930" height="346" alt="Bildschirmfoto 2025-11-10 um 17
54 35"
src="https://github.com/user-attachments/assets/f3b74011-c75c-448a-819e-80e7e8684e92"
/> |


Release Notes:

- Improved lifetime highlighting in Rust using the `lifetime` capture.
2025-11-10 19:41:14 +01:00
Danilo Leal
ed61a79cc5 agent_ui: Fix history view losing focus when empty (#42374)
Closes https://github.com/zed-industries/zed/issues/42356

This PR fixes the history view losing focus by simply always displaying
the search editor. I don't think it's too weird to not have it when it's
empty, and it also ends up matching how regular pickers work.

Release Notes:

- agent: Fixed a bug where navigating the agent panel with the keyboard
wouldn't work if you visited the history view and it was empty/had no
entries.
2025-11-10 15:29:55 -03:00
Tim Vermeulen
aa6270e658 editor: Add sticky scroll (#42242)
Closes #5344


https://github.com/user-attachments/assets/37ec58b0-7cf6-4eea-9b34-dccf03d3526b

Release Notes:

- Added a setting to stick scopes to the top of the editor

---------

Co-authored-by: KyleBarton <kjb@initialcapacity.io>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-10 11:24:30 -07:00
Dino
d896af2f15 git: Handle buffer file path changes (#41944)
Update `GitStore.on_buffer_store_event` so that, when a
`BufferStoreEvent::BufferChangedFilePath` event is received, we check if
there's any diff state for the buffer and, if so, update it according to
the new file path, in case the file exists in the repository.

Closes #40499

Release Notes:

- Fixed issue with git diff tracking when updating a buffer's file from
an untracked to a tracked file
2025-11-10 18:19:08 +00:00
Agus Zubiaga
c748b177c4 zeta2 cli: Cache at LLM request level (#42371)
We'll now cache LLM responses at the request level (by hash of
URL+contents) for both context and prediction. This way we don't need to
worry about mistakenly using the cache when we change the prompt or its
components.

Release Notes:

- N/A

---------

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-11-10 14:23:52 -03:00
Tryanks
ddf5937899 gpui: Move 'app closing on last window closed' behavior to app-side (#41436)
This commit is a continuation of #36548. As per [mikayla-maki's
Comment](https://github.com/zed-industries/zed/pull/36548#issuecomment-3412140698),
I removed the process management behavior located in GPUI and
reimplemented it in Zed.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-11-10 17:19:35 +00:00
Piotr Osiewicz
6e1d86f311 fs: Handle io::ErrorKind::NotADirectory in fs::metadata (#42370)
New error variants were stabilized in 1.83, and this might've led to us
mis-handling not-a-directory errors.

Co-authored-by: Dino <dino@zed.dev>

Release Notes:

- N/A

Co-authored-by: Dino <dino@zed.dev>
2025-11-10 18:18:40 +01:00
Abul Hossain Khan
a3f04e8b36 agent_ui: Fix thread history item showing GMT time instead of local time on Windows (#42198)
Closes #42178
Now it's consistent with the DateAndTime path which already does
timezone conversion.

- **Future Work**
Happy to tackle the TODO in `time_format.rs` about implementing native
Windows APIs for proper localized formatting (similar to macOS's
`CFDateFormatter`) as a follow-up.

Release Notes:

- agent: Fixed the thread history item timestamp, which was being shown
in GMT instead of in the user's local timezone on Windows.
2025-11-10 13:34:59 -03:00
Danilo Leal
3c81ee6ba6 agent_ui: Allow to configure a default model for profiles through modal (#42359)
Follow-up to https://github.com/zed-industries/zed/pull/39220

This PR allows to configure a default model for a given profile through
the profile management modal.

| Option In Picker | Model Selector |
|--------|--------|
| <img width="1172" height="538" alt="Screenshot 2025-11-10 at 12  24
2@2x"
src="https://github.com/user-attachments/assets/33dfb6f1-f8fd-42f9-b824-3dab807094da"
/> | <img width="1172" height="1120" alt="Screenshot 2025-11-10 at 12 
24@2x"
src="https://github.com/user-attachments/assets/50360b0a-fbb1-455e-9cf7-9fa987345038"
/> |

Release Notes:

- N/A
2025-11-10 13:12:13 -03:00
Miguel Raz Guzmán Macedo
35ae2f5b2b typo: Use tips from proselint (#42362)
I ran [proselint](https://github.com/amperser/proselint) (recommended by
cURL author [Daniel
Stenberg](https://daniel.haxx.se/blog/2022/09/22/taking-curl-documentation-quality-up-one-more-notch/))
against all the `.md` files in the codebase to see if I could fix some
easy typos.

The tool is noisier than I would like and picking up the overrides to
the default config in a `.proselintrc.json` was much harder than I
expected.

There's many other small nits [1] that I believe are best left to your
docs czar whenever they want to consider incorporating a tool like this
into big releases or CI, but these seemed like small wins for now to
open a conversation about a tool like proselint.

---

[1]: Such nits include
- incosistent 1 or 2 spaces
- "color" vs "colour"
- ab/use of `very` 
- awkward or superfluous phrasing.

Release Notes:

- N/A

Signed-off-by: mrg <miguelraz@ciencias.unam.mx>
2025-11-10 17:51:44 +02:00
Agus Zubiaga
d420dd63ed zeta: Improve unified diff prompt (#42354)
Extract some of the improvements from to the unified diff prompt from
https://github.com/zed-industries/zed/pull/42171 and adds some other
about how context work to improve the reliability of predictions.

We also now strip the `<|user_cursor|>` marker if it appears in the
output rather than failing.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-11-10 14:58:42 +00:00
feeiyu
42ed032f12 Fix circular reference issue between EditPredictionButton and PopoverMenuHandle (#42351)
Closes #ISSUE

While working on issue #40906, I discovered that RemoteClient was not
being released after the remote project closed.
Analysis revealed a circular reference between EditPredictionButton and
PopoverMenuHandle.

Dependency Chain: RemoteClient → Project → ZetaEditPredictionProvider →
EditPredictionButton ↔ PopoverMenuHandle

<img width="400" height="300" alt="image"
src="https://github.com/user-attachments/assets/6b716c9b-6938-471a-b044-397314b729d4"
/>

a) EditPredictionButton hold the reference of PopoverMenuHandle 

5f8226457e/crates/zed/src/zed.rs (L386-L394)

b) PopoverMenuHandle hold the reference of Fn which capture
`Entity<EditPredictionButton>`

5fc54986c7/crates/edit_prediction_button/src/edit_prediction_button.rs (L382-L389)


a9bc890497/crates/ui/src/components/popover_menu.rs (L376-L384)


Release Notes:

- N/A
2025-11-10 16:52:03 +02:00
David
2d84af91bf agent: Add ability to set a default_model per profile (#39220)
Split off from https://github.com/zed-industries/zed/pull/39175

Requires https://github.com/zed-industries/zed/pull/39219 to be merged
first

Adds support for `default_model` for profiles: 

```
      "my-profile": {
        "name": "Coding Agent",
        "tools": {},
        "enable_all_context_servers": false,
        "context_servers": {},
        "default_model": {
          "provider": "copilot_chat",
          "model": "grok-code-fast-1"
        }
      }
```

Which will then switch to the default model whenever the profile is
activated

![2025-09-30 17 09
06](https://github.com/user-attachments/assets/43f07b7b-85d9-4aff-82ce-25d6f5050d50)


Release Notes:

- Added `default_model` configuration to agent profile

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-11-10 11:11:24 -03:00
Abdugani Toshmukhamedov
7aacc7566c Add support for closing window tabs with middle mouse click (#41628)
This change adds support for closing a system window tabs by pressing
the middle mouse button.
It improves tab management UX by matching common tab behavior.

Release Notes:

- Added support for closing system window tabs with middle mouse click.
2025-11-10 15:09:37 +01:00
Caleb Van Dyke
8d632958db Add better labels for completions for ty lsp (#42233)
Verified that this works locally. I modeled it after how basedpyright
and pyright work. Here is a screenshot of what it looks like (issue has
screenshots of the old state):

<img width="593" height="258" alt="Screenshot 2025-11-07 at 2 40 50 PM"
src="https://github.com/user-attachments/assets/5d2371fc-360b-422f-ba59-0a95f2083c87"
/>

Closes #42232

Release Notes:

- python/ty: Code completion menu now shows packages that will be
imported when a given entry is accepted.
2025-11-10 13:28:12 +01:00
Lukas Wirth
0149de4b54 git: Fix panic in git2 due to empty repo paths (#42304)
Fixes ZED-1VR

Release Notes:

- Fixed sporadic panic in git features
2025-11-10 09:27:51 +00:00
Jakub Konka
359160c8b1 git: Add askpass delegate to git-commit handlers (#42239)
In my local setup, I always enforce git-commit signing with GPG/SSH
which automatically enforces `git commit -S` when committing. This
changeset will now show a modal to the user for them to specify the
passphrase (if any) so that they can unlock their private key for
signing when committing in Zed.

<img width="1086" height="948" alt="Screenshot 2025-11-07 at 11 09
09 PM"
src="https://github.com/user-attachments/assets/ac34b427-c833-41c7-b634-8781493f8a5e"
/>


Release Notes:

- Handle automatic git-commit signing by presenting the user with an
askpass modal
2025-11-10 07:57:50 +00:00
Max Brunsfeld
b8081ad7a6 Make it easy to point zeta2 at ollama (#42329)
I wanted to be able to work offline, so I made it a little bit more
convenient to point zeta2 at ollama.

* For zeta2, don't require that request ids be UUIDs
* Add an env var `ZED_ZETA2_OLLAMA` that sets the edit prediction URL
and model id to work w/ ollama.

Release Notes:

- N/A
2025-11-09 21:10:36 -08:00
ᴀᴍᴛᴏᴀᴇʀ
35c58151eb git: Fix support for self-hosted Bitbucket (#42002)
Closes #41995

Release Notes:

- Fixed support for self-hosted Bitbucket
2025-11-09 21:37:22 -05:00
Ayush Chandekar
e025ee6a11 git: Add base branch support to create_branch (#42151)
Closes [#41674](https://github.com/zed-industries/zed/issues/41674)

Description:
Creating a branch from a base requires switching to the base branch
first, then creating the new branch and checking out to it, which
requires multiple operations.

Add base_branch parameter to create_branch to allow a new branch from a
base branch in one operation which is synonymous to the command `git
switch -c <new-branch> <base-branch>`.

Below is the video after solving the issue: 

(`master` branch is the default branch here, and I create a branch
`new-branch-2` based off the `master` branch. I also show the error
which used to appear before the fix.)

[Screencast from 2025-11-07
05-14-32.webm](https://github.com/user-attachments/assets/d37d1b58-af5f-44e8-b867-2aa5d4ef3d90)

Release Notes:

- Fixed the branch-picking error by replacing multiple sequential switch
operations with just one switch operation.

Signed-off-by: ayu-ch <ayu.chandekar@gmail.com>
2025-11-09 21:35:29 -05:00
Mayank Verma
c60d31a726 git: Track worktree references to resolve stale repository state (#41592)
Closes #35997
Closes #38018
Closes #41516

Release Notes:
- Fixes stale git repositories persisting after removal
2025-11-09 21:24:20 -05:00
Bennet Bo Fenner
0bcf607a28 agent_ui: Always allow to include symbols (#42261)
We can always include symbols, since we either include a ResourceLink to
the symbol (when `PromptCapabilities::embedded_context = false`) or a
Resource (when `PromptCapabilities::embedded_context = true`)

Release Notes:

- Fixed an issue where symbols could not be included when using specific
ACP agents
2025-11-09 20:45:00 +01:00
Bennet Bo Fenner
431a195c32 acp: Fix issue with mentions when embedded_context is set to false (#42260)
Release Notes:

- acp: Fixed an issue where Zed would not respect
`PromptCapabilities::embedded_context`
2025-11-09 20:44:07 +01:00
Danilo Leal
6db6251484 agent_ui: Fix external agent icons in configuration view (#42313)
This PR makes the icons for external agents in the configuration view
use `from_external_svg` instead of `from_path`.

Release Notes:

- N/A
2025-11-09 14:32:19 -03:00
Danilo Leal
2fb3d593bc agent_ui: Add component to standardize the configured LLM card (#42314)
This PR adds a new component to the `language_models` crate called
`ConfiguredApiCard`:

<img width="500" height="420" alt="Screenshot 2025-11-09 at 2  07@2x"
src="https://github.com/user-attachments/assets/655ea941-2df8-4489-a4da-bba34acf33a9"
/>

We were previously recreating this component from scratch with regular
divs in all LLM providers render function, which was redundant as they
all essentially looked the same and didn't have any major variations
aside from labels. We can clean up a bunch of similar code with this
change, which is cool!

Release Notes:

- N/A
2025-11-09 14:32:05 -03:00
chenmi
cc1d66b530 agent_ui: Improve API key configuration UI display (#42306)
Improve the layout and text display of API key configuration in multiple
language model providers to ensure proper text wrapping and ellipsis
handling when API URLs are long.

Before:

<img width="320" alt="image"
src="https://github.com/user-attachments/assets/2f89182c-34a0-4f95-a43a-c2be98d34873"
/>

After:

<img width="320" alt="image"
src="https://github.com/user-attachments/assets/09bf5cc3-07f0-47bc-b21a-d84b8b1caa67"
/>

Changes include:
- Add proper flex layout with overflow handling
- Replace truncate_and_trailoff with CSS text ellipsis
- Ensure consistent UI behavior across all providers

Release Notes:

- Improved API key configuration display in language model settings
2025-11-09 13:00:31 -03:00
Lukas Wirth
5d08c1b35f Surpress more rust-analyzer error logs (#42299)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-09 11:27:16 +00:00
Lukas Wirth
b7d4d1791a diagnostics: Keep diagnostic excerpt ranges properly ordered (#42298)
Fixes ZED-2CQ

We were doing the binary search by buffer points, but due to await
points within this function we could end up mixing points of differing
buffer versions.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-09 10:55:56 +00:00
John Tur
81d38d9872 Additional Windows keyboard input fixes (#42294)
- Enable Alt+Numpad input
- For this to be effective, the default keybindings for Alt+{Number}
will need to be unbound. This won't be needed once we gain the ability
to differentiate numpad digit keys from alphanumeric digit keys.
  - Fixes https://github.com/zed-industries/zed/issues/40699
- Fix a number of edge cases with dead keys

Release Notes:

- N/A
2025-11-09 03:51:39 -05:00
Matt Miller
21f73d9c02 Use ButtonLike and add OpenExcerptsSplit and dispatches on click (#42283)
Closes #42099 

Release Notes:

- N/A
2025-11-08 17:47:01 -06:00
Danilo Leal
12857a7207 agent: Improve AddSelectionToThread action display (#42280)
Closes https://github.com/zed-industries/zed/issues/42276

This fixes the fact that the `AddSelectionToThread` action was visible
when `disable_ai` was true, as well as it improves its display by making
it either disabled or hidden when there are no selections in the editor.
I also ended up removing it from the app menu simply because making it
observe the `disable_ai` setting would be a bit more complex than I'd
like at the moment, so figured that, given I'm also now adding it to the
toolbar selection menu, we could do without it over there.

Release Notes:

- Fixed the `AddSelectionToThread` action showing up when `disable_ai`
is true
- Improved the `AddSelectionToThread` action display by only making it
available when there are selections in the editor
2025-11-08 14:41:08 -03:00
Danilo Leal
f6be16da3b docs: Update text threads page (#42273)
Adding a section clarifying the difference between regular threads vs.
text threads.

Release Notes:

- N/A
2025-11-08 12:55:31 -03:00
Danilo Leal
94aa643484 docs: Update agent tools page (#42271)
Release Notes:

- N/A
2025-11-08 12:54:42 -03:00
Jakub Konka
28a85158c7 shell_env: Wrap error context in format! where missing (#42267)
Release Notes:

- N/A
2025-11-08 15:46:01 +00:00
boris.zalman
a2c2c617b5 Add helix keymap to delete without yanking (#41988)
Added previously missing keymap for helix deleting without yank. Action
"editor::Delete" is mapped to "Ald-d" as in
https://docs.helix-editor.com/master/keymap.html#changes

Release Notes:

- N/A
2025-11-08 15:53:34 +01:00
Xipeng Jin
77667f4844 Remove Markdown CodeBlock metadata and Custom rendering (#42211)
Follow up #40736

Clean up `CodeBlockRenderer::Custom` related rendering per the previous
PR
[comment](https://github.com/zed-industries/zed/pull/40736#issuecomment-3503074893).
Additional note here:
1. The `Custom` variant in the enum `CodeBlockRenderer` will become not
useful since cleaning all code related to the custom rendering logic.
2. Need to further review the usage of code block `metadata` field in
`MarkdownTag::CodeBlock` enum.

I would like to have the team further review my note above so that we
can make sure it will be safe to clean it up and will not affect any
potential future features will be built on top of it. Thank you!

Release Notes:

- N/A
2025-11-08 14:00:53 +01:00
Hyeondong Lee
b01a6fbdea Fix missing highlight for macro_invocation bang (#41572)
(Not sure if this was left out on purpose, but this makes things feel a
bit more consistent since [VS Code parses bang mark as part of the macro
name](https://github.com/microsoft/vscode/blob/main/extensions/rust/syntaxes/rust.tmLanguage.json#L889-L905))


Release Notes:
- Added the missing highlight for the bang mark in macro invocations.


| **Before** | **After** |
| :---: | :---: |
| <img width="684" height="222" alt="before"
src="https://github.com/user-attachments/assets/ae71eda3-76b5-4547-b2df-4e437a07abf5"
/> | <img width="646" height="236" alt="fixed"
src="https://github.com/user-attachments/assets/500deda5-d6d8-439c-8824-65c2fb0a5daa"
/> |
2025-11-08 08:42:33 +00:00
Roland Rodriguez
44d91c1709 docs: Explain what scrollbar marks represent (#42130)
## Summary

Adds explanations for what each type of scrollbar indicator visually
represents in the editor.

## Description

This PR addresses the issue where users didn't understand what the
colored marks on the scrollbar mean. The existing documentation
explained how to toggle each type of mark on/off, but didn't explain
what they actually represent.

This adds a brief, clear explanation after each scrollbar indicator
setting describing what that indicator shows (e.g., "Git diff indicators
appear as colored marks showing lines that have been added, modified, or
deleted compared to the git HEAD").

## Fixes

Closes #31794

## Test Plan

- Documentation follows the existing style and format of
`docs/src/configuring-zed.md`
- Each explanation is concise and immediately follows the setting
description
- Language is clear and user-friendly

Release Notes:

- N/A
2025-11-08 10:40:18 +02:00
Donnie Adams
d187cbb188 Add comment injection support to remaining languages (#41710)
Release Notes:

- Added support for comment language injections for remaining built-in
languages and multi-line support for Rust
2025-11-08 10:34:53 +02:00
Agus Zubiaga
c241eadbc3 zeta2: Targeted retrieval search (#42240)
Since we removed the filtering step during context gathering, we want
the model to perform more targeted searches. This PR tweaks search tool
schema allowing the model to search within syntax nodes such as `impl`
blocks or methods.

This is what the query schema looks like now:

```rust
/// Search for relevant code by path, syntax hierarchy, and content.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchToolQuery {
    /// 1. A glob pattern to match file paths in the codebase to search in.
    pub glob: String,
    /// 2. Regular expressions to match syntax nodes **by their first line** and hierarchy.
    ///
    /// Subsequent regexes match nodes within the full content of the nodes matched by the previous regexes.
    ///
    /// Example: Searching for a `User` class
    ///     ["class\s+User"]
    ///
    /// Example: Searching for a `get_full_name` method under a `User` class
    ///     ["class\s+User", "def\sget_full_name"]
    ///
    /// Skip this field to match on content alone.
    #[schemars(length(max = 3))]
    #[serde(default)]
    pub syntax_node: Vec<String>,
    /// 3. An optional regular expression to match the final content that should appear in the results.
    ///
    /// - Content will be matched within all lines of the matched syntax nodes.
    /// - If syntax node regexes are provided, this field can be skipped to include as much of the node itself as possible.
    /// - If no syntax node regexes are provided, the content will be matched within the entire file.
    pub content: Option<String>,
}
```

We'll need to keep refining this, but the core implementation is ready.

Release Notes:

- N/A

---------

Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-11-08 01:06:12 +00:00
Mikayla Maki
5f8226457e Automate settings registration (#42238)
Release Notes:

- N/A

---------

Co-authored-by: Nia <nia@zed.dev>
2025-11-07 22:27:14 +00:00
Anthony Eid
309947aa53 editor: Allow clicking on excerpts with alt key to open file path (#42235)
#42021 Made clicking on an excerpt title toggle it. This PR brings back
the old behavior if a user is pressing the Alt key when clicking on an
excerpt title.

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-11-07 16:23:55 -05:00
Lukas Wirth
0881e548de terminal: Spawn terminal process on main thread on unix (#42234)
Otherwise the terminal will not process the signals correctly 

Release Notes:

- Fixed ctrl+c and friends not working in the terminal on macOS and
linux
2025-11-07 20:56:43 +00:00
claytonrcarter
4511d11a11 bundle: Skip sentry upload for local install on macOS (#42231)
This is a follow up to #41482. When running `script/bundle-mac`, it will
upload debug symbols to Sentry if you have a `$SENTRY_AUTH_TOKEN` set. I
happen to have one set, so this script was trying to generate and upload
those. Whoops! This change skips the upload entirely if you're running a
local install.

Release Notes:

- N/A
2025-11-07 12:38:01 -08:00
Conrad Irwin
19d2fdb6c6 Refresh releases page post deploy (#42218)
Release Notes:

- N/A
2025-11-07 13:34:47 -07:00
Conrad Irwin
20953ecb9d Make nightly bucket objects public (#42229)
Makes it easier to port updates to cloudflare

Closes #ISSUE

Release Notes:

- N/A
2025-11-07 19:43:08 +00:00
Anthony Eid
7475bdaf20 debugger: Truncate scope names to avoid text overlapping in variable list (#42230)
Closes #41969

This was caused because scope names weren't being truncated unlike the
other type of variable list entries.

Release Notes:

- debugger: Fix bug where minimizing the width of the variable list
would cause scope names to overlap

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-11-07 19:42:14 +00:00
Dima
8e4c807c6a Fix duplicated 'the' typo (#42225)
Just found this while reading the docs.

Release Notes:

- N/A
2025-11-07 21:37:10 +02:00
John Tur
a66dac7b3a Fix crash during drag-and-drop on Windows (#42227)
The HGLOBAL is itself the HDROP. Do not dereference it.

Release Notes:

- windows: Fixed crashes during drag-and-drop operations
2025-11-07 19:18:47 +00:00
morgankrey
8a903f9c10 2025 11 07 update privacy docs (#42226)
Docs update

Release Notes:

- N/A
2025-11-07 13:16:13 -06:00
Lukas Wirth
9f9575d100 Silence rust-analyzer startup errors (#42222)
When rust-analyzer is still loading the cargo project it tends to error
out on most lsp requests with `content modified`. This pollutes our
logs.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 18:43:43 +00:00
Marshall Bowers
00898d46c0 docs: Add docs on extension capabilities (#42223)
This PR adds some initial docs on extension capabilities.

Release Notes:

- N/A
2025-11-07 18:38:20 +00:00
Joseph T. Lyons
bcc3307a7e Add optional Zed log field to all bug report templates (#42221)
Release Notes:

- N/A
2025-11-07 18:28:35 +00:00
Lukas Wirth
8ba33ad270 gpui: Do not unwrap in window_procedure (#42216)
Technically these should not be possible to hit, but sentry says
otherwise. Turning these into errors should give us more information
than the abort due to unwinding across ffi boundaries.

Fixes ZED-321

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 18:10:30 +00:00
Marshall Bowers
1e6344899d docs: Update extension features language (#42215)
This PR updates the language around what features extensions can
provide.

Release Notes:

- N/A
2025-11-07 17:43:22 +00:00
Jakub Konka
93f9cff876 Remove invalid assertion in editor (#42210)
Release Notes:

- N/A
2025-11-07 17:36:10 +00:00
Lukas Wirth
6cafe4a9c5 gpui: Do not panic when unable to find the selected fonts (#42212)
Fixes ZED-329

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 17:08:37 +00:00
Lukas Wirth
585c440e6e util: Support shell env fetching for git bash (#42208)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 16:28:41 +00:00
Lukas Wirth
083bd147ef util: Fall back to cmd if we can't find powershell on the system (#42204)
Closes https://github.com/zed-industries/zed/issues/42165

Release Notes:

- Fixed trying to use powershell for commands when its not installed on
the system
2025-11-07 17:13:52 +01:00
Remco Smits
9591790d8d markdown: Add support for HTML styling attributes (#42143)
Second take on https://github.com/zed-industries/zed/pull/37765.

This PR adds support for styling elements (**b**, **strong**, **em**,
**i**, **ins**, **del**), but also allow you to show the styling text
inline with the current text.
This is done by appending all the up-following text into one text chunk
and merge the highlights from both of them into the already existing
chunk. If there does not exist a text chunk, we will create one and the
next iteration we will use that one to store all the information on.

**Before**
<img width="483" height="692" alt="Screenshot 2025-11-06 at 22 08 09"
src="https://github.com/user-attachments/assets/6158fd3b-066c-4abe-9f8e-bcafae85392e"
/>

**After**
<img width="868" height="300" alt="Screenshot 2025-11-06 at 22 08 21"
src="https://github.com/user-attachments/assets/4d5a7a33-d31c-4514-91c8-2b2a2ff43e0e"
/>

**Code example**
```html
<p>some text <b>bold text</b></p>
<p>some text <strong>strong text</strong></p>
<p>some text <i>italic text</i></p>
<p>some text <em>emphasized text</em></p>
<p>some text <del>delete text</del></p>
<p>some text <ins>insert text</ins></p>

<p>Some text <strong>strong text</strong> more text <b>bold text</b> more text <i>italic text</i> more text <em>emphasized text</em> more text <del>deleted text</del> more text <ins>inserted text</ins></p>

<p><a href="https://example.com">Link Text</a></p>

<p style="text-decoration: underline;">text styled from style attribute</p>
```

cc @bennetbo 

**TODO**
- [x] add tests for styling nested text that should result in one merge

Release Notes:

- Markdown Preview: Added support for `HTML` styling elements

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-11-07 16:12:27 +00:00
Lukas Wirth
74bf1a170d recent_projects: Do not try to watch /etc/ssh/ssh_config on windows (#42200)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 15:46:37 +00:00
Bennet Bo Fenner
e72c3bf20d agent_ui: Remove message_editor module (#42195)
Artefact from agent1 removal

Release Notes:

- N/A
2025-11-07 14:36:58 +00:00
Maokaman1
160bf915aa language_models: Filter out whitespace-only text content parts for OpenAI and OpenAI compatible providers (#40316)
Closes #40097

When multiple files are added sequentially to the agent panel, the
request JSON incorrectly includes "text" elements containing only
spaces. These empty elements cause the Zhipu AI API to return a "text
cannot be empty" error.
The fix filters out any "text" elements that are empty or contain only
whitespaces.

UI state when the error occurs:
<img width="300" alt="Image"
src="https://github.com/user-attachments/assets/c55e5272-3f03-42c0-b412-fa24be2b0043"
/>

Request JSON (causing the error):
```
{
  "model": "glm-4.6",
  "messages": [
    {
      "role": "system",
      "content": "<<CUT>>"
    },
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "[@1.txt](zed:///agent/file?path=C%3A%5CTemp%5CTest%5C1.txt)"
        },
        { "type": "text", "text": " " },
        {
          "type": "text",
          "text": "[@2.txt](zed:///agent/file?path=C%3A%5CTemp%5CTest%5C2.txt)"
        },
        { "type": "text", "text": " describe" },
```

Release Notes:

- Fixed an issue when an OpenAI request contained whitespace-only text content

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-11-07 14:43:07 +01:00
Xipeng Jin
aff4c25a47 markdown: Restore horizontal scrollbars for codeblocks (#40736)
### Summary

Restore the agent pane’s code-block horizontal scrollbar for easier
scrolling without trackpad and preserve individual scroll state across
multiple code blocks.

### Motivation

Addresses https://github.com/zed-industries/zed/issues/34224, where
agent responses with wide code snippets couldn’t be scrolled
horizontally in the panel. Previously there is no visual effect for
scrollbar to let the user move the code snippet and it was not obviously
to use trackpad or hold down `shift` while scrolling. This PR will
ensure the user being able to only use their mouse to drag the
horizontal scrollbar to show the complete line when the code overflow
the width of code block.

### Changes

- Support auto-hide horizontal scrollbar for rendering code block in
agent panel by adding scrollbar support in markdown.rs
- Add `code_block_scroll_handles` cache in
_crates/markdown/src/markdown.rs_ to give each code block a persistent
`ScrollHandle`.
- Wrap rendered code blocks with custom horizontal scrollbars that match
the vertical scrollbar styling and track hover visibility.
- Retain or clear scroll handles based on whether horizontal overflow is
enabled, preventing leaks when the markdown re-renders.

### How to Test

1. Open the agent panel, request code generation, and ensure wide
snippets show a horizontal scrollbar on hover.
3. Scroll horizontally, navigate away (e.g., change tabs or trigger a
re-render), and confirm the scroll position sticks when returning.
5. Toggle horizontal overflow styling off/on (if applicable) and verify
scrollbars appear or disappear appropriately.

### Screenshots / Demos (if UI change)


https://github.com/user-attachments/assets/e23f94d9-8fe3-42f5-8f77-81b1005a14c8

### Notes for Reviewers

- This is my first time contribution for `zed`, sorry for any code
patten inconsistency. So please let me know if you have any comments and
suggestions to make the code pattern consistent and easy to maintain.
- For now, the horizontal scrollbar is not configurable from the setting
and the style is fixed with the same design as the vertical one. I am
happy to readjust this setting to fit the needs.
- Please let me know if you think any behaviors or designs need to be
changed for the scrollbar.
- All changes live inside _crates/markdown/src/markdown.rs_; no API
surface changes.

Closes #34224 

### Release Notes:

- AI: Show horizontal scroll-bars in wide markdown elements
2025-11-07 13:35:59 +00:00
aohanhongzhi
146e754f73 URL-encode the image paths in Markdown so that images with filenames (#41788)
Closes https://github.com/zed-industries/zed/issues/41786

Release Notes:

- markdown preview: Fixed an issue where path urls would not be parsed
correctly when containing URL-encoded characters

<img width="1680" height="1126"
alt="569415cb-b3e8-4ad6-b31c-a1898ec32085"
src="https://github.com/user-attachments/assets/7de8a892-ff01-4e00-a28c-1c5e9206ce3a"
/>
2025-11-07 14:02:40 +01:00
Kirill Bulatov
278fe91a9a Skip buffer registration if lsp data should be ignored (#42190)
Release Notes:

- N/A
2025-11-07 12:57:54 +00:00
Lukas Wirth
39fb89e031 workspace: Do not panic when the database is corruped (#42186)
Fixes ZED-1NK

Release Notes:

- Fixed zed not starting when the database cannot be loaded
2025-11-07 12:36:36 +00:00
Casper van Elteren
9d52b6c538 terminal: Allow configuring conda manager (#40577)
Closes #40576
This PR makes Conda activation configurable and transparent by adding a
`terminal.detect_venv.on.conda_manager` setting (`"auto" | "conda" |
"mamba" | "micromamba"`, default `"auto"`), updating Python environment
activation to honor this preference (or the detected manager executable)
and fall back to `conda` when necessary.

The preference is passed via `ZED_CONDA_MANAGER` from the terminal
settings, and the activation command is built accordingly (with proper
quoting for paths). Changes span
`zed/crates/terminal/src/terminal_settings.rs` (new `CondaManager` and
setting), `zed/crates/project/src/terminals.rs` (inject env var),
`zed/crates/languages/src/python.rs` (activation logic), and
`zed/assets/settings/default.json` (document the setting). Default
behavior remains unchanged for most users while enabling explicit
selection of `mamba` or `micromamba`.

Release Notes:
- Added: terminal.detect_venv.on.conda_manager setting to choose the
Conda manager (auto, conda, mamba, micromamba). Default: auto.
- Changed: Python Conda environment activation now respects the
configured manager, otherwise uses the detected environment manager
executable, and falls back to conda.
- Reliability: Activation commands quote manager paths to handle spaces
across platforms.
- Compatibility: No breaking changes; non-Conda environments are
unaffected; remote terminals are supported.

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-11-07 12:35:06 +00:00
Mikayla Maki
082b80ec89 gpui: Unify the index_for_x methods (#42162)
Supersedes https://github.com/zed-industries/zed/pull/39910

At some point, these two (`index_for_x` and `closest_index_for_x`)
methods where separated out and some code paths used one, while other
code paths took the other. That said, their behavior is almost
identical:

- `index_for_x` computes the index behind the pixel offset, and returns
`None` if there's an overshoot
- `closest_index_for_x` computes the nearest index to the pixel offset,
taking into account whether the offset is over halfway through or not.
If there's an overshoot, it returns the length of the line.

Given these two behaviors, `closest_index_for_x` seems to be a more
useful API than `index_for_x`, and indeed the display map and other core
editor features use it extensively. So this PR is an experiment in
simply replacing one behavior with the other.

Release Notes:

- Improved the accuracy of mouse selections in Markdown
2025-11-07 14:23:43 +02:00
Bennet Bo Fenner
483e31e42a Fix telemetry (#42184)
Follow up to #41991 🤦🏻 

Release Notes:

- N/A
2025-11-07 11:36:05 +00:00
Bennet Bo Fenner
61c263fcf0 agent_ui: Allow opening thread as markdown in remote projects (#42182)
Release Notes:

- Added support for opening thread as markdown in remote projects
2025-11-07 11:32:16 +00:00
Lukas Wirth
3c19174f7b diagnostics: Fix diagnostics view no clearing blocks correctly (#42179)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 11:16:25 +00:00
Bennet Bo Fenner
7e93c171b5 action_log: Remove unused code (#42177)
Release Notes:

- N/A
2025-11-07 10:33:51 +00:00
Lukas Wirth
88a8e53696 editor: Remove buffer and display map fields from SelectionsCollection (#42175)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 11:21:14 +01:00
Bennet Bo Fenner
c2416d6bab Add agent metrics (#41991)
Release Notes:

- N/A
2025-11-07 10:07:57 +00:00
Jason Lee
29cc3d0e18 gpui: Add to support check state to MenuItem (#39876)
Release Notes:

- N/A

---


https://github.com/user-attachments/assets/d46b77ae-88ba-43da-93ad-3656a7fecaf9

The system menu is only support for macOS, so here just modify the macOS
platform special code.

The Windows, Linux used `ApplicationMenu`, I have already added
`checked` option to Zed's ContextMenu.

Then later when this PR merged, we can improve "View" menu to show check
state to panels (Project Panel, Outline Panel, ...).
2025-11-07 09:42:55 +00:00
Remco Smits
760747f127 markdown: Add support for HTML table captions (#41192)
Thanks to @Angelk90 for pointing out that, we were missing this feature.
So this PR implement the caption feature for HTML tables for the
markdown preview.

**Code example**
```html
<table>
    <caption>Revenue by Region</caption>
    <tr>
        <th rowspan="2">Region</th>
        <th colspan="2">Revenue</th>
        <th rowspan="2">Growth</th>
    </tr>
    <tr>
        <th>Q2 2024</th>
        <th>Q3 2024</th>
    </tr>
    <tr>
        <td>North America</td>
        <td>$2.8M</td>
        <td>$2.4B</td>
        <td>+85,614%</td>
    </tr>
    <tr>
        <td>Europe</td>
        <td>$1.2M</td>
        <td>$1.9B</td>
        <td>+158,233%</td>
    </tr>
    <tr>
        <td>Asia-Pacific</td>
        <td>$0.5M</td>
        <td>$1.4B</td>
        <td>+279,900%</td>
    </tr>
</table>
```

**Result**:
<img width="1201" height="774" alt="Screenshot 2025-10-25 at 21 18 01"
src="https://github.com/user-attachments/assets/c2a8c1c2-f861-40df-b5c9-549932818f6e"
/>

Release Notes:

- Markdown preview: Added support for `HTML` table captions
2025-11-07 09:47:12 +01:00
Lukas Wirth
d4ec55b183 editor: Correctly handle blocks spanning more than 128 rows (#42172)
Release Notes:

- Fixed block rendering for blocks spanning more than 128 rows
2025-11-07 08:46:57 +00:00
Max Brunsfeld
f89bb2f0d2 Small zeta cli fixes (#42170)
* Fix a panic that happened because we lost the
`ContextRetrievalStarted` debug message, so we didn't assign `t0`.
* Write the edit prediction response log file as a markdown file
containing the text, not a JSON file. We mostly always want the text
content.

Release Notes:

- N/A
2025-11-07 07:34:05 +00:00
Conrad Irwin
e43c436cb6 Create sentry releases in after_release (#42169)
This had been moved to auto-release preview, and was not running for
stable.

Closes #ISSUE

Release Notes:

- N/A
2025-11-07 07:30:10 +00:00
Lukas Wirth
de1bf64f41 project: Remove unnecessary panic (#42167)
If we are in a remote session with the remote dropped, this path is very
much reachable if the call to this function got queued up in a task.

Fixes ZED-124

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-07 06:56:19 +00:00
Jakub Konka
00eafe63d9 git: Make long-running git staging snappy in git panel (#42149)
Previously, staging a large file in the git panel would block the UI
items until that operation finished. This is due to the fact that
staging is a git op that is locked globally by git (per repo) meaning
only one op that is modifying the git index can run at any one time. In
order to make the UI snappy while letting any pending git staging jobs
to finish in the background, we track their progress via `PendingOps`
indexed by git entry path. We have already had a concept of pending
operations however they existed at the UI layer in the `GitPanel`
abstraction. This PR moves and augments `PendingOps` into the model
`Repository` in `git_store` which seems like a more natural place for
tracking running git jobs/operations. Thanks to this, pending ops are
now stored in a `SumTree` indexed by git entry path part of the
`Repository` snapshot, which makes for efficient access from the UI.

Release Notes:

- Improved UI responsiveness when staging/unstaging large files in the
git panel
2025-11-07 07:34:06 +01:00
Max Brunsfeld
5044e6ac1d zeta2: Make eval example file format more expressive (#42156)
* Allow expressing alternative possible context fetches in `Expected
Context` section
* Allow marking a subset of lines as "required" in `Expected Context`.

We still need to improve how we display the results. I've removed the
context pass/fail pretty printing for now, because it would need to be
rethought to work with the new structure, but for now I think we should
focus on getting basic predictions to run. But this is progress toward a
better structure for eval examples.

Release Notes:

- N/A

---------

Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-11-06 18:05:18 -08:00
Max Brunsfeld
784fdcaee3 zeta2: Build edit prediction prompt and process model output in client (#41870)
Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-11-06 18:36:58 -05:00
Ben Kunkle
fb87972f44 settings_ui: Use any open workspace window when opening settings links (#42106)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 18:36:23 -05:00
Julia Ryan
8cccb5d4f5 Use windows runner for publishing winget package (#42144)
Our new linux runners don't have powershell installed which causes the
`release-winget` job to fail. This simply runs that step on windows
instead.

Release Notes:

- N/A
2025-11-06 14:13:03 -08:00
Andrew Farkas
2895d31d83 Fix tab switcher close item using wrong pane (#42138)
Closes #40646

Release Notes:

- Fixed `tab_switcher::CloseSelectedItem` doing nothing on tab in
inactive pane

Co-authored-by: Cole Miller <cole@zed.dev>
2025-11-06 20:38:52 +00:00
Cave Bats Of Ware
a112153a2e Enable image support in remote projects (#39158)
Adds support for opening and displaying images in remote projects. The
server streams image data to the client in chunks, where the client then
reconstructs the image and displays it. This change includes:

- Adding `image` crate as a dependency for remote_server
- Implementing `ImageStore` for remote access
- Creating proto definitions for image-related messages
- Adding handlers for creating images for peers
- Computing image metadata from bytes instead of reading from disk for
remote images

Closes #20430
Closes #39104
Closes #40445

Release Notes:

- Added support for image preview in remote sessions.
- Fixed #39104

<img width="982" height="551" alt="image"
src="https://github.com/user-attachments/assets/575428a3-9144-4c1f-b76f-952019ea14cc"
/>
<img width="978" height="547" alt="image"
src="https://github.com/user-attachments/assets/fb58243a-4856-4e73-bb30-8d5e188b3ac9"
/>

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-11-06 11:31:32 -08:00
Richard Feldman
d21184b1d3 Fix ACP extension root dir (#42131)
Agents running in extensions need to have a root directory of the
extension's dir for installation and authentication, but *not* for the
conversation itself - otherwise the agent is running things like
terminal commands in the wrong dir.

Release Notes:

- Fixed Agent Server extensions having the current working directory of
the extension rather than the project
2025-11-06 19:19:16 +00:00
Anthony Eid
ba136abf6c editor: Add action to move between snippet tabstop positions v2 (#42127)
Closes https://github.com/zed-industries/zed/issues/41407

This PR fixes the issues that caused #41407 to be reverted in #42008.
Namely that the action context didn't take into account if a snippet
could move backwards or forwards, and the action shared the same key
mapping as `editor::MoveToPreviousWordStart` and
`editor::MoveToNextWordEnd`.

I changed the default key mapping for the move to snippet tabstop to tab
and shift-tab to match the default behavior of other editors.

Release Notes:

- Editor: Add actions to move between snippet tabstop positions
2025-11-06 18:49:39 +00:00
Lukas Wirth
2d45c23fb0 remote: Flush to stdin when writing to sftp 2 (#42126)
https://github.com/zed-industries/zed/pull/42103#issuecomment-3498137130

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 18:18:19 +00:00
Bo Lorentsen
b78f19982f Allow for using config specific gdb binaries in gdb adapter (#37193)
I really need this for embedded development in IDF and Zephyr, where the
chip vendors sometimes provide there own specialized version of the
tools, and I need to direct zed to use these.

The current GDB adapter only supports the gdb it find in the normal
search path, and it also seems like we where not able to transfer gdb
specific startup arguments (only `-i=dap` is set) .

In order to fix this I (semi wipe using GPT-4.1) expanded the GDB
adapter with 2 new config options :

* **gdb_path** holds a full path to the gdb executable, for now only
full path or relative to cwd
* **gdb_args** an array holding additional arguments given to gdb on
startup
 
It seemed to me, like the `env` config did not transferred to gdb, so
this is added to.
 
 I have tested this locally, and it seems to work not only compile :-)

Release Notes:

debugger: Adds gdb_path and gdb_args to gdb debug adapter options 
debugger: Fix bug where gdb debug sessions wouldn't inherit the shell
environment from Zed

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-11-06 12:53:59 -05:00
Lukas Wirth
a7fac65d62 gpui: Remove unneeded weak Rc cycle in WindowsWindowInner (#42119)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 17:33:09 +00:00
Lukas Wirth
bf8864d106 gpui: Remove all (unsound) ManuallyDrop usages, panic on device loss (#42114)
Given that when we lose our devices unrecoverably we will panic anyways,
might as well do so eagerly which makes it clearer.

Additionally this PR replaces all uses of `ManuallyDrop` with `Option`,
as otherwise we need to do manual bookkeeping of what is and isn't
initialized when we try to recover devices as we can bail out halfway
while recovering. In other words, the code prior to this was fairly
unsound due to freely using `ManuallyDrop::drop`.
 
Fixes ZED-1SS
 
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 17:29:15 +00:00
Nia
08ee4f7966 notify: Bump to rebased 8.2.0 fork (#42113)
Hopefully makes progress towards #38109, #39266

Release Notes:

- N/A
2025-11-06 16:30:17 +00:00
Lukas Wirth
6f6f652cf2 zlog: Add env var to enable line number logging (#41905)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 15:31:26 +00:00
Lukas Wirth
9c8e37a156 clangd: Fix switch source header action on windows (#42105)
Fixes https://github.com/zed-industries/zed/issues/40935

Release Notes:

- Fixed clangd's switch source header action not working on windows
2025-11-06 15:12:34 +00:00
Kirill Bulatov
d54c64f35a Refresh outline panel on file renames (#42104)
Closes https://github.com/zed-industries/zed/issues/41877

Release Notes:

- Fixed outline panel not updating file headers on rename
2025-11-06 14:46:46 +00:00
Lukas Wirth
0b53da18d5 remote: Flush to stdin when writing to sftp (#42103)
https://github.com/zed-industries/zed/issues/42027#issuecomment-3497210172

Release Notes:

- Fixed ssh remoting potentially failing due to not flushing stdin to
sftp
2025-11-06 14:27:26 +00:00
Libon
5ced3ef0fd editor: Improve multibuffer header spacing (#42071)
BEFORE:
<img width="1751" height="629" alt="image"
src="https://github.com/user-attachments/assets/f88464d3-5daa-4d53-b394-f92db8b0fd8c"
/>

AFTER:
<img width="1714" height="493" alt="image"
src="https://github.com/user-attachments/assets/022c883b-b219-40a3-aa5f-0c16d23e8abf"
/>

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-11-06 14:09:06 +00:00
ʟᴜɴᴇx
7a37dd9433 Do not serialize buffers containing bundled files (#38102)
Fix bundled file persistence by introducing SerializationMode enum

Closes: #38094

What's the issue?

Opening bundled files like Default Key Bindings
(zed://settings/keymap-default.json) was causing SQLite foreign key
constraint errors. The editor was trying to save state for these
read-only assets just like regular files, but since bundled files don't
have database entries, the foreign key constraint would fail.

The fix

Replaced the boolean serialize_dirty_buffers flag with a type-safe
SerializationMode enum:

```rust
pub enum SerializationMode {
    Enabled,   // Regular files persist across sessions
    Disabled,  // Bundled files don't persist
}
```

This prevents serialization at the source: workspace_id() returns None
for disabled editors, serialize() bails early, and should_serialize()
returns false. When opening bundled files, we set the mode to Disabled
from the start, so they're treated as transient views that never
interact with the persistence layer.

Changes

- editor.rs: Added SerializationMode enum and updated serialization
methods to respect it
- items.rs: Guarded should_serialize() to prevent disabled editors from
being serialized
- zed.rs: Set SerializationMode::Disabled in open_bundled_file()

Result

Bundled files open cleanly without SQLite errors and don't persist
across workspace reloads (expected behavior). Regular file persistence
remains unaffected.

Release Notes: Fixed SQLite foreign key constraint errors when opening
bundled files like Default Key Bindings.

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-11-06 13:30:03 +00:00
Danilo Leal
a7163623e7 agent_ui: Make "add more agents" menu item take to extensions (#42098)
Now that agent servers are a thing, this is the primary and easiest way
to quickly add more agents to Zed, without touching any settings JSON
file. :)

Release Notes:

- N/A
2025-11-06 09:55:06 -03:00
Lukas Wirth
f08068680d agent_ui: Do not show Codex wsl warning on wsl take 2 (#42096)
https://github.com/zed-industries/zed/pull/42079#discussion_r2498472887

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 12:15:17 +00:00
Lukas Wirth
a951e414d8 util: Fix shell environment fetching with cmd (#42093)
Release Notes:

- Fixed shell environment fetching failing when having `cmd` configured
as terminal shell
2025-11-06 12:07:34 +00:00
Lukas Wirth
e75c6b1aa5 remote: Fix detect_can_exec detection (#42087)
Closes https://github.com/zed-industries/zed/issues/42036

Release Notes:

- Fixed an issuer with wsl exec detection eagerly failing, breaking
remote connections
2025-11-06 12:06:32 +00:00
Kirill Bulatov
149eedb73d Fix scroll position restoration (#42088)
Follow-up of https://github.com/zed-industries/zed/pull/42035
Scroll position needs to be stored immediately, otherwise editor close
may not register that.

Release Notes:

- N/A
2025-11-06 11:37:27 +00:00
Lukas Wirth
fb46bae3ed remote: Add missing quotation in extract_server_binary (#42085)
Also respect the shell env for various commands again
Should close https://github.com/zed-industries/zed/issues/42027

Release Notes:

- Fixed remote server installation failing on some setups
2025-11-06 11:19:24 +00:00
Lukas Wirth
28d7c37b0d recent_projects: Improve user facing error messages on connection failure (#42083)
cc https://github.com/zed-industries/zed/issues/42004

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 11:00:41 +00:00
Lukas Wirth
f6da987d4c agent_ui: Do not show Codex wsl warning on wsl (#42079)
Release Notes:

- Fixed the codex wsl warning being shown on wsl itself
2025-11-06 10:34:14 +00:00
Kirill Bulatov
efc71f35a5 Tone down extension errors (#42080)
Before:
<img width="2032" height="1161" alt="before"
src="https://github.com/user-attachments/assets/5c497b47-87e8-4167-bc28-93e34556ea4d"
/>

After:
<img width="2032" height="1161" alt="after"
src="https://github.com/user-attachments/assets/4a87803f-67df-4bf8-ade0-306f3c9ca81e"
/>

Release Notes:

- N/A
2025-11-06 10:12:04 +00:00
Lukas Wirth
3b7ee58cfa zed: Attach console to parent process before processing --printenv (#42075)
Otherwise the `--printenv` flag will simply not work on windows

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 09:47:54 +00:00
Lukas Wirth
32047bef93 text: Improve panic messages with more information (#42072)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 09:16:45 +00:00
Lukas Wirth
4003287cc3 vim: Downgrade user config error from panic to log (#42070)
Fixes ZED-2W3

Release Notes:

- Fixed panic due to invalid vim keycap
2025-11-06 08:37:04 +00:00
Lukas Wirth
001a47c8b7 gpui: Inline some hot recursive scope functions (#42069)
This reduces stack usage in prepainting slightly as we stack less
references to window/app.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-06 08:31:39 +00:00
Lukas Wirth
113f0780b3 agent_ui: Fix string slicing panic in message editor (#42068)
Fixes ZED-302

Release Notes:

- Fixed a panic in agent message editor when using multibyte whitespace
characters
2025-11-06 07:58:20 +00:00
Lexi Mattick
273321608f gpui: Add debug assertion to Window::on_action and make docs consistent (#41202)
Improves formatting consistency across various docs, fixes some typos,
and adds a missing `debug_assert_paint` to `Window::on_action` and
`Window::on_action_when`.

Release Notes:

- N/A
2025-11-06 07:55:20 +00:00
Smit Barmase
92cfce568b language: Fix completion menu no longer prioritizes relevant items for Typescript and Python (#42065)
Closes #41672

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

Release Notes:

- Fixed issue where completion menu no longer prioritizes relevant items
for TypeScript and Python.
2025-11-06 13:19:46 +05:30
Coenen Benjamin
2b6cf31ace file_finder: Display duplicated file in file finder history (#41917)
Closes #41850

When digging into this I figured out that basically what was going on is
in the history of the file finder it doesn't update the name of the file
duplicated because when you duplicate a file it's named automatically
with `filename copy` and so this filename was added to the history but
not updated so once you wanted to go back into this file it was not part
of file finder displayed history anymore because this file doesn't exist
anymore but the entity id remains the same.
I was also to reproduce this bug when just renaming a file.

Release Notes:

- Fixed: Display duplicated file in file finder history

---------

Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-11-06 07:06:45 +00:00
Lemon
58cec41932 Adjust terminal decorative character ranges to include missing Powerline characters (#42043)
Closes #41975.

This change adjusts some of the ranges which were incorrectly labeled or
excluded characters. The new ranges include three codepoints which are
not assigned in Nerd Fonts. However, one of these codepoints were
already included prior to this change. These codepoints are:

- U+E0C9, between the two ice separators
- U+E0D3, between the two trapezoid separators. The ranges prior to this
PR already included this one.
- U+E0D5, between the trapezoid separators and the inverted triangle
separators

I included these so as to not overcomplicate the ranges by
cherry-picking the defined codepoints. That being said, if we're okay
with this and an additional unassigned codepoint (U+E0CB, between ice
separators and honeycomb separators) being included then a simple range
from 0xE0B0 to 0xE0D7 nicely includes all of the Powerline characters.

I wasn't sure how to write tests for this so I just added two characters
to the existing tests which were previously not covered lol. All of the
Powerline characters can be seen
[here](https://www.nerdfonts.com/cheat-sheet) by searching `nf-pl`.

Release Notes:

- Fixed certain Powerline characters incorrectly having terminal
contrast adjustment applied.
2025-11-06 06:38:34 +00:00
Conrad Irwin
2ec5ca0e05 Fix generate release notes script on first stable (#42061)
Don't crash in generate-release-notes on the first stable
commit on a branch.

Release Notes:

- N/A
2025-11-05 23:25:30 -07:00
Conrad Irwin
f8da550867 Refresh zed.dev releases page after releases (#42060)
Release Notes:

- N/A
2025-11-05 23:25:13 -07:00
Mayank Verma
0b1d3d78a4 git: Fix pull failing when tracking remote with different branch name (#41768)
Closes #31430

Release Notes:

- Fixed git pull failing when tracking remote with different branch name

Here's a before/after comparison when `dev` branch has upstream set to
`origin/main`:


https://github.com/user-attachments/assets/3a47e736-c7b7-4634-8cd1-aca7300c3a73
2025-11-06 05:18:08 +00:00
Cole Miller
930b489d90 ci: Don't require protobuf and postgres checks for tests_pass for now (#42057)
For now, there are cases where we want to merge PRs (advisedly) even
though these checks fail.

Release Notes:

- N/A
2025-11-06 00:03:50 -05:00
Delvin
121cee8045 git: Add cursor pointer on last commit to check changes (#41960)
Release Notes:
-  Improved visual cue on git panel ui to check previous commit changes 

Before: 
<img width="1470" height="956" alt="Screenshot 2025-11-05 at 2 06 49 pm"
src="https://github.com/user-attachments/assets/b8c54bb6-c8b8-4d36-a14f-71d725ed68f2"
/>

After:
<img width="1470" height="956" alt="Screenshot 2025-11-05 at 2 06 24 pm"
src="https://github.com/user-attachments/assets/d8d96f9e-ceed-4c02-9f93-de9fd3dfcbf1"
/>
2025-11-05 23:27:38 -05:00
Viraj Bhartiya
5360dc1504 Refactor timestamp formatting in Git UI components to use chrono for local time calculations (#41005)
- Updated `blame_ui.rs`, `branch_picker.rs`, `commit_tooltip.rs`, and
`commit_view.rs` to replace the previous timestamp formatting with
`chrono` for better accuracy in local time representation.
- Introduced `chrono::Local::now().offset().local_minus_utc()` to obtain
the local offset for timestamp formatting.

Closes #40878

Release Notes:
- Improved timestamp handling in various Git UI components for enhanced
user experience.
2025-11-05 23:23:22 -05:00
Danilo Leal
69862790cb docs: Improve content in /ai/agent-panel and /ai/rules (#42055)
- Clarify about first-party supported features in external agents
- Create new section for selection as context for higher visibility
- Add keybindings for agent profiles
- Fix outdated setting using `assistant` instead of `agent`
- Add keybinding for accessing the rules library

Release Notes:

- N/A
2025-11-06 01:18:55 -03:00
ᴀᴍᴛᴏᴀᴇʀ
284d8f790a Support Forgejo and Gitea avatars in git blame (#41813)
Part of #11043.

Codeberg is a public instance of Forgejo, as confirmed by the API
documentation at https://codeberg.org/api/swagger. Therefore, I renamed
the related component from codeberg to forgejo and added codeberg.org as
a public instance.

Furthermore, to optimize request speed for the commit API, I set
`stat=false&verification=false&files=false`.

<img width="1650" height="1268" alt="CleanShot 2025-11-03 at 19 57
06@2x"
src="https://github.com/user-attachments/assets/c1b4129e-f324-41c2-86dc-5e4f7403c046"
/>

<br/>
<br/>

Regarding Gitea Support: 

Forgejo is a fork of Gitea, and their APIs are currently identical
(e.g., for getting avatars). However, to future-proof against potential
API divergence, I decided to treat them as separate entities. The
current gitea implementation is essentially a copy of the forgejo file
with the relevant type names and the public instance URL updated.

Release Notes:

- Added Support for Forgejo and Gitea avatars in git blame
2025-11-05 22:53:28 -05:00
Danilo Leal
f824e93eeb docs: Remove non-existing keybinding in /visual-customization (#42053)
Release Notes:

- N/A
2025-11-06 00:38:39 -03:00
Danilo Leal
e71bc4821c docs: Add section about agent servers in /external-agents (#42052)
Release Notes:

- N/A
2025-11-06 00:38:33 -03:00
Jason Lee
64c8c19e1b gpui: Impl Default for TextRun (#41084)
Release Notes:

- N/A

When I was implementing Input, I often used `TextRun`, but `background`,
`underline` and `strikethrough` were often not used.

So make change to simplify it.
2025-11-06 01:50:23 +00:00
Richard Feldman
622d626a29 Add agent-servers.md (#41609)
Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2025-11-06 01:35:56 +00:00
Petros Amoiridis
714481073d Fix macOS new window stacking (#38683)
Closes #36206 

Disclaimer: I did use AI for help to end up with this proposed solution.
😅

## Observed behavior of native apps on macOS (like Safari)

I first did a quick research on how Safari behaves on macOS, and here's
what I have found:

1. Safari seems to position new windows with an offset based on the
currently active window
2. It keeps opening new windows with an offset until the new window
cannot fit the display bounds horizontally, vertically or both.
3. When it cannot fit horizontally, the new window opens at x=0
(y=active window's y)
4. When it cannot fit vertically, the new window opens at y=0 (x=active
window's x)
5. When it cannot fit both horizontally and vertically, the new window
opens at x=0 and y=0 (top left).
6. At any moment if I activate a different Safari window, the next new
window is offset off of that
7. If I resize the active window and open a new window, the new window
has the same size as the active window

So, I implemented the changes based on those observations.

I am not sure if touching `gpui/src/window.rs` is the way to go. I am
open to feedback and direction here.

I am also not sure if making my changes platform (macOS) specific, is
the right thing to do. I reckoned that Linux and Windows have different
default behaviors, and the original issue mentioned macOS. But,
likewise, I am open to take a different approach.

## Tests

I haven't included tests for such change, as it seems to me a bit
difficult to properly test this, other than just doing a manual
integration test. But if you would want them for such a change, happy to
try including them.

## Alternative approach

I also did some research on macOS native APIs that we could use instead
of trying to make the calculations ourselves, and I found
`NSWindow.cascadeTopLeftFromPoint` which seems to be doing exactly what
we want, and more. It probably takes more things into consideration and
thus it is more robust. We could go down that road, and add it to
`gpui/src/platform/mac/window.rs` and then use it for new window
creation. Again, if that's what you would do yourselves, let me know and
I can either change the implementation here, or open a new pull request
and let you decide which one would you would like to pursue.

## Video showing the behavior


https://github.com/user-attachments/assets/f802a864-7504-47ee-8c6b-8d9b55474899

🙇‍♂️

Release Notes:

- Improved macOS new window stacking
2025-11-06 01:22:49 +00:00
630 changed files with 34261 additions and 13110 deletions

View File

@@ -39,3 +39,21 @@ body:
Output of "zed: copy system specs into clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -33,3 +33,21 @@ body:
Output of "zed: copy system specs into clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -33,3 +33,21 @@ body:
Output of "zed: copy system specs into clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -33,3 +33,21 @@ body:
Output of "zed: copy system specs into clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -56,3 +56,20 @@ body:
Output of "zed: copy system specs into clipboard"
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your `Zed.log` file to this issue.
description: |
From the command palette, run `zed: open log` to see the last 1000 lines.
Or run `zed: reveal log in file manager` to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false

View File

@@ -1,4 +1,4 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Feature Request

View File

@@ -4,10 +4,8 @@ description: "Runs the tests"
runs:
using: "composite"
steps:
- name: Install Rust
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

View File

@@ -11,9 +11,8 @@ runs:
using: "composite"
steps:
- name: Install test runner
shell: powershell
working-directory: ${{ inputs.working-directory }}
run: cargo install cargo-nextest --locked
uses: taiki-e/install-action@nextest
- name: Install Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

104
.github/workflows/after_release.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
# Generated from xtask::workflows::after_release
# Rebuild with `cargo xtask workflows`.
name: after_release
on:
release:
types:
- published
jobs:
rebuild_releases_page:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: after_release::rebuild_releases_page::refresh_cloud_releases
run: curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag=${{ github.event.release.tag_name }}
shell: bash -euxo pipefail {0}
- name: after_release::rebuild_releases_page::redeploy_zed_dev
run: npm exec --yes -- vercel@37 --token="$VERCEL_TOKEN" --scope zed-industries redeploy https://zed.dev
shell: bash -euxo pipefail {0}
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
post_to_discord:
needs:
- rebuild_releases_page
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-release-url
name: after_release::post_to_discord::get_release_url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- id: get-content
name: after_release::post_to_discord::get_content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: '...'
- name: after_release::post_to_discord::discord_webhook_action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish_winget:
runs-on: self-32vcpu-windows-2022
steps:
- id: set-package-name
name: after_release::publish_winget::set_package_name
run: |
if ("${{ github.event.release.prerelease }}" -eq "true") {
$PACKAGE_NAME = "ZedIndustries.Zed.Preview"
} else {
$PACKAGE_NAME = "ZedIndustries.Zed"
}
echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT
shell: pwsh
- name: after_release::publish_winget::winget_releaser
uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
create_sentry_release:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: release::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
notify_on_failure:
needs:
- rebuild_releases_page
- post_to_discord
- publish_winget
- create_sentry_release
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}

View File

@@ -42,7 +42,7 @@ jobs:
exit 1
;;
esac
which cargo-set-version > /dev/null || cargo install cargo-edit
which cargo-set-version > /dev/null || cargo install cargo-edit -f --no-default-features --features "set-version"
output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')"
export GIT_COMMITTER_NAME="Zed Bot"
export GIT_COMMITTER_EMAIL="hi@zed.dev"

View File

@@ -1,6 +1,7 @@
# Generated from xtask::workflows::cherry_pick
# Rebuild with `cargo xtask workflows`.
name: cherry_pick
run-name: 'cherry_pick to ${{ inputs.channel }} #${{ inputs.pr_number }}'
on:
workflow_dispatch:
inputs:
@@ -16,6 +17,10 @@ on:
description: channel
required: true
type: string
pr_number:
description: pr_number
required: true
type: string
jobs:
run_cherry_pick:
runs-on: namespace-profile-2x4-ubuntu-2404

View File

@@ -1,7 +1,7 @@
name: "Close Stale Issues"
on:
schedule:
- cron: "0 7,9,11 * * 3"
- cron: "0 8 31 DEC *"
workflow_dispatch:
jobs:
@@ -15,14 +15,15 @@ jobs:
stale-issue-message: >
Hi there! 👋
We're working to clean up our issue tracker by closing older issues that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and we will keep it open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, we'll close it in 7 days.
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
days-before-stale: 120
days-before-close: 7
any-of-issue-labels: "bug,panic / crash"
days-before-stale: 60
days-before-close: 14
only-issue-types: "Bug,Crash"
operations-per-run: 1000
ascending: true
enable-statistics: true
stale-issue-label: "stale"
exempt-issue-labels: "never stale"

View File

@@ -1,93 +0,0 @@
# IF YOU UPDATE THE NAME OF ANY GITHUB SECRET, YOU MUST CHERRY PICK THE COMMIT
# TO BOTH STABLE AND PREVIEW CHANNELS
name: Release Actions
on:
release:
types: [published]
jobs:
discord_release:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- name: Get release URL
id: get-release-url
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
URL="https://zed.dev/releases/preview"
else
URL="https://zed.dev/releases/stable"
fi
echo "URL=$URL" >> "$GITHUB_OUTPUT"
- name: Get content
uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1
id: get-content
with:
stringToTruncate: |
📣 Zed [${{ github.event.release.tag_name }}](<${{ steps.get-release-url.outputs.URL }}>) was just released!
${{ github.event.release.body }}
maxLength: 2000
truncationSymbol: "..."
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
publish-winget:
runs-on:
- ubuntu-latest
steps:
- name: Set Package Name
id: set-package-name
run: |
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
PACKAGE_NAME=ZedIndustries.Zed.Preview
else
PACKAGE_NAME=ZedIndustries.Zed
fi
echo "PACKAGE_NAME=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
- uses: vedantmgoyal9/winget-releaser@19e706d4c9121098010096f9c495a70a7518b30f # v2
with:
identifier: ${{ steps.set-package-name.outputs.PACKAGE_NAME }}
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
send_release_notes_email:
if: false && github.repository_owner == 'zed-industries' && !github.event.release.prerelease
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
fetch-depth: 0
- name: Check if release was promoted from preview
id: check-promotion-from-preview
run: |
VERSION="${{ github.event.release.tag_name }}"
PREVIEW_TAG="${VERSION}-pre"
if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then
echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT"
else
echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT"
fi
- name: Send release notes email
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
run: |
TAG="${{ github.event.release.tag_name }}"
cat << 'EOF' > release_body.txt
${{ github.event.release.body }}
EOF
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
> release_data.json
curl -X POST "https://zed.dev/api/send_release_notes_email" \
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
-H "Content-Type: application/json" \
-d @release_data.json

View File

@@ -35,9 +35,11 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::install_hyperfine
run: cargo install hyperfine
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: compare_perf::run_perf::install_hyperfine
uses: taiki-e/install-action@hyperfine
- name: steps::git_checkout
run: git fetch origin ${{ inputs.base }} && git checkout ${{ inputs.base }}
shell: bash -euxo pipefail {0}

View File

@@ -12,7 +12,7 @@ on:
- main
jobs:
danger:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo

View File

@@ -43,9 +43,7 @@ jobs:
fetch-depth: 0
- name: Install cargo nextest
shell: bash -euxo pipefail {0}
run: |
cargo install cargo-nextest --locked
uses: taiki-e/install-action@nextest
- name: Limit target directory size
shell: bash -euxo pipefail {0}

138
.github/workflows/extension_tests.yml vendored Normal file
View File

@@ -0,0 +1,138 @@
# Generated from xtask::workflows::extension_tests
# Rebuild with `cargo xtask workflows`.
name: extension_tests
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: '1'
CARGO_INCREMENTAL: '0'
ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf
on:
workflow_call:
inputs:
run_tests:
description: Whether the workflow should run rust tests
required: true
type: boolean
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
fetch-depth: ${{ github.ref == 'refs/heads/main' && 2 || 350 }}
- id: filter
name: filter
run: |
if [ -z "$GITHUB_BASE_REF" ]; then
echo "Not in a PR context (i.e., push to main/stable/preview)"
COMPARE_REV="$(git rev-parse HEAD~1)"
else
echo "In a PR context comparing to pull_request.base.ref"
git fetch origin "$GITHUB_BASE_REF" --depth=350
COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)"
fi
CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})"
check_pattern() {
local output_name="$1"
local pattern="$2"
local grep_arg="$3"
echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \
echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \
echo "${output_name}=false" >> "$GITHUB_OUTPUT"
}
check_pattern "check_rust" '^(Cargo.lock|Cargo.toml|.*\.rs)$' -qP
check_pattern "check_extension" '^.*\.scm$' -qP
shell: bash -euxo pipefail {0}
outputs:
check_rust: ${{ steps.filter.outputs.check_rust }}
check_extension: ${{ steps.filter.outputs.check_extension }}
check_rust:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_rust == 'true'
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: extension_tests::run_clippy
run: cargo clippy --release --all-targets --all-features -- --deny warnings
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
if: inputs.run_tests
uses: taiki-e/install-action@nextest
- name: steps::cargo_nextest
if: inputs.run_tests
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
shell: bash -euxo pipefail {0}
timeout-minutes: 3
check_extension:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_extension == 'true'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: cache-zed-extension-cli
name: extension_tests::cache_zed_extension_cli
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: zed-extension
key: zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}
- name: extension_tests::download_zed_extension_cli
if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true'
run: |
wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
chmod +x zed-extension
shell: bash -euxo pipefail {0}
- name: extension_tests::check
run: |
mkdir -p /tmp/ext-scratch
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
tests_pass:
needs:
- orchestrate
- check_rust
- check_extension
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && always()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: run_tests::tests_pass
run: |
set +x
EXIT_CODE=0
check_result() {
echo "* $1: $2"
if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi
}
check_result "orchestrate" "${{ needs.orchestrate.result }}"
check_result "check_rust" "${{ needs.check_rust.result }}"
check_result "check_extension" "${{ needs.check_extension.result }}"
exit $EXIT_CODE
shell: bash -euxo pipefail {0}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -10,7 +10,7 @@ on:
- v*
jobs:
run_tests_mac:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
@@ -29,9 +29,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
@@ -45,7 +42,7 @@ jobs:
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_linux:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -57,16 +54,19 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
@@ -75,8 +75,7 @@ jobs:
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
@@ -90,7 +89,7 @@ jobs:
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_windows:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
@@ -109,9 +108,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: pwsh
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh
@@ -125,7 +121,7 @@ jobs:
shell: pwsh
timeout-minutes: 60
check_scripts:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
@@ -154,7 +150,7 @@ jobs:
shell: bash -euxo pipefail {0}
timeout-minutes: 60
create_draft_release:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
@@ -202,6 +198,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -242,6 +241,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -475,14 +477,20 @@ jobs:
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: release::create_sentry_release
uses: getsentry/action-release@526942b68292201ac6bbb99b9a0747d4abee354c
with:
environment: production
notify_on_failure:
needs:
- upload_release_assets
- auto_release_preview
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SENTRY_ORG: zed-dev
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -12,7 +12,7 @@ on:
- cron: 0 7 * * *
jobs:
check_style:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
@@ -28,7 +28,7 @@ jobs:
shell: bash -euxo pipefail {0}
timeout-minutes: 60
run_tests_windows:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: self-32vcpu-windows-2022
steps:
- name: steps::checkout_repo
@@ -47,9 +47,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: pwsh
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh
@@ -93,6 +90,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -140,6 +140,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -358,7 +361,7 @@ jobs:
needs:
- check_style
- run_tests_windows
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-32x64-ubuntu-2004
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
@@ -389,7 +392,7 @@ jobs:
needs:
- check_style
- run_tests_windows
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: self-mini-macos
env:
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
@@ -431,7 +434,7 @@ jobs:
- bundle_mac_x86_64
- bundle_windows_aarch64
- bundle_windows_x86_64
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-4x8-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -487,3 +490,21 @@ jobs:
SENTRY_PROJECT: zed
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
timeout-minutes: 60
notify_on_failure:
needs:
- bundle_linux_aarch64
- bundle_linux_x86_64
- bundle_mac_aarch64
- bundle_mac_x86_64
- bundle_windows_aarch64
- bundle_windows_x86_64
if: failure()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: release::notify_on_failure::notify_slack
run: |-
curl -X POST -H 'Content-type: application/json'\
--data '{"text":"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}' "$SLACK_WEBHOOK"
shell: bash -euxo pipefail {0}
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_WORKFLOW_FAILURES }}

View File

@@ -6,24 +6,21 @@ env:
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1'
MODEL_NAME: ${{ inputs.model_name }}
on:
pull_request:
types:
- synchronize
- reopened
- labeled
branches:
- '**'
schedule:
- cron: 0 0 * * *
workflow_dispatch: {}
workflow_dispatch:
inputs:
model_name:
description: model_name
required: true
type: string
jobs:
agent_evals:
if: |
github.repository_owner == 'zed-industries' &&
(github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run-eval'))
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -40,6 +37,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
@@ -49,14 +49,19 @@ jobs:
run: cargo build --package=eval
shell: bash -euxo pipefail {0}
- name: run_agent_evals::agent_evals::run_eval
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
run: cargo run --package=eval -- --repetitions=8 --concurrency=1 --model "${MODEL_NAME}"
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 60
timeout-minutes: 600
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -34,6 +34,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}
@@ -74,6 +77,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/bundle-linux
run: ./script/bundle-linux
shell: bash -euxo pipefail {0}

View File

@@ -0,0 +1,68 @@
# Generated from xtask::workflows::run_cron_unit_evals
# Rebuild with `cargo xtask workflows`.
name: run_cron_unit_evals
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
on:
schedule:
- cron: 47 1 * * 2
workflow_dispatch: {}
jobs:
cron_unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
- name: ./script/run-unit-evals
run: ./script/run-unit-evals
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
- name: run_agent_evals::cron_unit_evals::send_failure_to_slack
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

View File

@@ -15,7 +15,7 @@ on:
- v[0-9]+.[0-9]+.x
jobs:
orchestrate:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
@@ -59,7 +59,7 @@ jobs:
run_nix: ${{ steps.filter.outputs.run_nix }}
run_tests: ${{ steps.filter.outputs.run_tests }}
check_style:
if: github.repository_owner == 'zed-industries'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-4x8-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -113,9 +113,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: pwsh
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh
@@ -143,16 +140,19 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
@@ -161,8 +161,7 @@ jobs:
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
@@ -197,9 +196,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
@@ -232,6 +228,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::setup_cargo_config
run: |
mkdir -p ./../.cargo
@@ -263,16 +262,19 @@ jobs:
mkdir -p ./../.cargo
cp ./.cargo/ci-config.toml ./../.cargo/config.toml
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::setup_linux
run: ./script/linux
shell: bash -euxo pipefail {0}
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cache_rust_dependencies_namespace
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: cargo build -p collab
run: cargo build -p collab
shell: bash -euxo pipefail {0}
@@ -285,40 +287,6 @@ jobs:
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
timeout-minutes: 60
check_postgres_and_protobuf_migrations:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files
run: git clean -df
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge
run: |
if [ -z "$GITHUB_BASE_REF" ];
then
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
else
git checkout -B temp
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
fi
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action
uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
uses: bufbuild/buf-breaking-action@v1
with:
input: crates/proto/proto/
against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/
timeout-minutes: 60
check_dependencies:
needs:
- orchestrate
@@ -382,6 +350,9 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with:
@@ -518,6 +489,40 @@ jobs:
shell: bash -euxo pipefail {0}
timeout-minutes: 60
continue-on-error: true
check_postgres_and_protobuf_migrations:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: self-mini-macos
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
- name: run_tests::check_postgres_and_protobuf_migrations::remove_untracked_files
run: git clean -df
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::ensure_fresh_merge
run: |
if [ -z "$GITHUB_BASE_REF" ];
then
echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV"
else
git checkout -B temp
git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp"
echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV"
fi
shell: bash -euxo pipefail {0}
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_setup_action
uses: bufbuild/buf-setup-action@v1
with:
version: v1.29.0
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
uses: bufbuild/buf-breaking-action@v1
with:
input: crates/proto/proto/
against: https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/
timeout-minutes: 60
tests_pass:
needs:
- orchestrate
@@ -527,14 +532,13 @@ jobs:
- run_tests_mac
- doctests
- check_workspace_binaries
- check_postgres_and_protobuf_migrations
- check_dependencies
- check_docs
- check_licenses
- check_scripts
- build_nix_linux_x86_64
- build_nix_mac_aarch64
if: github.repository_owner == 'zed-industries' && always()
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && always()
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: run_tests::tests_pass
@@ -554,7 +558,6 @@ jobs:
check_result "run_tests_mac" "${{ needs.run_tests_mac.result }}"
check_result "doctests" "${{ needs.doctests.result }}"
check_result "check_workspace_binaries" "${{ needs.check_workspace_binaries.result }}"
check_result "check_postgres_and_protobuf_migrations" "${{ needs.check_postgres_and_protobuf_migrations.result }}"
check_result "check_dependencies" "${{ needs.check_dependencies.result }}"
check_result "check_docs" "${{ needs.check_docs.result }}"
check_result "check_licenses" "${{ needs.check_licenses.result }}"

View File

@@ -1,17 +1,26 @@
# Generated from xtask::workflows::run_agent_evals
# Generated from xtask::workflows::run_unit_evals
# Rebuild with `cargo xtask workflows`.
name: run_agent_evals
name: run_unit_evals
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: '0'
RUST_BACKTRACE: '1'
ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }}
ZED_EVAL_TELEMETRY: '1'
MODEL_NAME: ${{ inputs.model_name }}
on:
schedule:
- cron: 47 1 * * 2
workflow_dispatch: {}
workflow_dispatch:
inputs:
model_name:
description: model_name
required: true
type: string
commit_sha:
description: commit_sha
required: true
type: string
jobs:
unit_evals:
run_unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204
steps:
- name: steps::checkout_repo
@@ -33,9 +42,11 @@ jobs:
- name: steps::install_mold
run: ./script/install-mold
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
run: cargo install cargo-nextest --locked
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
@@ -44,20 +55,15 @@ jobs:
shell: bash -euxo pipefail {0}
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- name: run_agent_evals::unit_evals::send_failure_to_slack
if: ${{ failure() }}
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52
with:
method: chat.postMessage
token: ${{ secrets.SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN }}
payload: |
channel: C04UDRNNJFQ
text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
UNIT_EVAL_COMMIT: ${{ inputs.commit_sha }}
- name: steps::cleanup_cargo_config
if: always()
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}
cancel-in-progress: true

200
Cargo.lock generated
View File

@@ -32,6 +32,7 @@ dependencies = [
"settings",
"smol",
"task",
"telemetry",
"tempfile",
"terminal",
"ui",
@@ -39,6 +40,7 @@ dependencies = [
"util",
"uuid",
"watch",
"zlog",
]
[[package]]
@@ -79,6 +81,7 @@ dependencies = [
"rand 0.9.2",
"serde_json",
"settings",
"telemetry",
"text",
"util",
"watch",
@@ -93,6 +96,7 @@ dependencies = [
"auto_update",
"editor",
"extension_host",
"fs",
"futures 0.3.31",
"gpui",
"language",
@@ -211,6 +215,8 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -226,7 +232,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.6.3"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -243,7 +251,6 @@ dependencies = [
"acp_tools",
"action_log",
"agent-client-protocol",
"agent_settings",
"anyhow",
"async-trait",
"client",
@@ -315,10 +322,12 @@ dependencies = [
"assistant_slash_command",
"assistant_slash_commands",
"assistant_text_thread",
"async-fs",
"audio",
"buffer_diff",
"chrono",
"client",
"clock",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@@ -334,8 +343,10 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"gpui_tokio",
"html_to_markdown",
"http_client",
"image",
"indoc",
"itertools 0.14.0",
"jsonschema",
@@ -1231,15 +1242,15 @@ dependencies = [
[[package]]
name = "async_zip"
version = "0.0.17"
version = "0.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6"
dependencies = [
"async-compression",
"crc32fast",
"futures-lite 2.6.1",
"pin-project",
"thiserror 1.0.69",
"thiserror 2.0.17",
]
[[package]]
@@ -1324,10 +1335,14 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"clock",
"ctor",
"db",
"futures 0.3.31",
"gpui",
"http_client",
"log",
"parking_lot",
"paths",
"release_channel",
"serde",
@@ -1338,6 +1353,7 @@ dependencies = [
"util",
"which 6.0.3",
"workspace",
"zlog",
]
[[package]]
@@ -1449,6 +1465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
"zeroize",
]
@@ -3194,7 +3211,9 @@ dependencies = [
"indoc",
"ordered-float 2.10.1",
"rustc-hash 2.1.1",
"schemars 1.0.4",
"serde",
"serde_json",
"strum 0.27.2",
]
@@ -3674,6 +3693,7 @@ dependencies = [
"collections",
"futures 0.3.31",
"gpui",
"http_client",
"log",
"net",
"parking_lot",
@@ -5298,6 +5318,7 @@ dependencies = [
"serde_json",
"settings",
"supermaven",
"sweep_ai",
"telemetry",
"theme",
"ui",
@@ -5847,6 +5868,7 @@ dependencies = [
"lsp",
"parking_lot",
"pretty_assertions",
"proto",
"semantic_version",
"serde",
"serde_json",
@@ -6235,7 +6257,7 @@ dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"spin",
"spin 0.9.8",
]
[[package]]
@@ -6346,9 +6368,9 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "fork"
version = "0.2.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
checksum = "30268f1eefccc9d72f43692e8b89e659aeb52e84016c3b32b6e7e9f1c8f38f94"
dependencies = [
"libc",
]
@@ -6400,7 +6422,7 @@ dependencies = [
"ignore",
"libc",
"log",
"notify 8.0.0",
"notify 8.2.0",
"objc",
"parking_lot",
"paths",
@@ -7087,7 +7109,6 @@ dependencies = [
"askpass",
"buffer_diff",
"call",
"chrono",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@@ -7275,6 +7296,7 @@ dependencies = [
"calloop",
"calloop-wayland-source",
"cbindgen",
"circular-buffer",
"cocoa 0.26.0",
"cocoa-foundation 0.2.0",
"collections",
@@ -7330,6 +7352,7 @@ dependencies = [
"slotmap",
"smallvec",
"smol",
"spin 0.10.0",
"stacksafe",
"strum 0.27.2",
"sum_tree",
@@ -7793,6 +7816,7 @@ dependencies = [
"parking_lot",
"serde",
"serde_json",
"serde_urlencoded",
"sha2",
"tempfile",
"url",
@@ -8643,23 +8667,25 @@ dependencies = [
[[package]]
name = "jupyter-protocol"
version = "0.6.0"
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095"
dependencies = [
"anyhow",
"async-trait",
"bytes 1.10.1",
"chrono",
"futures 0.3.31",
"serde",
"serde_json",
"thiserror 2.0.17",
"uuid",
]
[[package]]
name = "jupyter-websocket-client"
version = "0.9.0"
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4197fa926a6b0bddfed7377d9fed3d00a0dec44a1501e020097bd26604699cae"
dependencies = [
"anyhow",
"async-trait",
@@ -8668,6 +8694,7 @@ dependencies = [
"jupyter-protocol",
"serde",
"serde_json",
"tokio",
"url",
"uuid",
]
@@ -8705,7 +8732,6 @@ dependencies = [
"ui",
"ui_input",
"util",
"vim",
"workspace",
"zed_actions",
]
@@ -8857,9 +8883,11 @@ dependencies = [
"icons",
"image",
"log",
"open_ai",
"open_router",
"parking_lot",
"proto",
"schemars 1.0.4",
"serde",
"serde_json",
"settings",
@@ -9024,6 +9052,7 @@ dependencies = [
"settings",
"smol",
"task",
"terminal",
"text",
"theme",
"toml 0.8.23",
@@ -9057,7 +9086,7 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
"spin 0.9.8",
]
[[package]]
@@ -9671,6 +9700,7 @@ dependencies = [
"settings",
"theme",
"ui",
"urlencoding",
"util",
"workspace",
]
@@ -9998,6 +10028,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniprofiler_ui"
version = "0.1.0"
dependencies = [
"gpui",
"serde_json",
"smol",
"util",
"workspace",
"zed_actions",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -10202,8 +10244,9 @@ dependencies = [
[[package]]
name = "nbformat"
version = "0.10.0"
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89c7229d604d847227002715e1235cd84e81919285d904ccb290a42ecc409348"
dependencies = [
"anyhow",
"chrono",
@@ -10405,11 +10448,10 @@ dependencies = [
[[package]]
name = "notify"
version = "8.0.0"
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
version = "8.2.0"
source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419"
dependencies = [
"bitflags 2.9.4",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.11.0",
"kqueue",
@@ -10418,7 +10460,7 @@ dependencies = [
"mio 1.1.0",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -10435,7 +10477,7 @@ dependencies = [
[[package]]
name = "notify-types"
version = "2.0.0"
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419"
[[package]]
name = "now"
@@ -10490,11 +10532,10 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
@@ -10997,6 +11038,7 @@ dependencies = [
"serde_json",
"settings",
"strum 0.27.2",
"thiserror 2.0.17",
]
[[package]]
@@ -13036,6 +13078,23 @@ dependencies = [
"zlog",
]
[[package]]
name = "project_benchmarks"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"client",
"futures 0.3.31",
"gpui",
"http_client",
"language",
"node_runtime",
"project",
"settings",
"watch",
]
[[package]]
name = "project_panel"
version = "0.1.0"
@@ -13063,6 +13122,7 @@ dependencies = [
"settings",
"smallvec",
"telemetry",
"tempfile",
"theme",
"ui",
"util",
@@ -13967,6 +14027,7 @@ dependencies = [
"gpui",
"gpui_tokio",
"http_client",
"image",
"json_schema_store",
"language",
"language_extension",
@@ -13980,6 +14041,7 @@ dependencies = [
"paths",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rayon",
"release_channel",
@@ -14225,7 +14287,7 @@ dependencies = [
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"untrusted 0.9.0",
"windows-sys 0.52.0",
]
@@ -14354,9 +14416,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.8"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
@@ -14406,25 +14468,26 @@ dependencies = [
[[package]]
name = "runtimelib"
version = "0.25.0"
source = "git+https://github.com/ConradIrwin/runtimed?rev=7130c804216b6914355d15d0b91ea91f6babd734#7130c804216b6914355d15d0b91ea91f6babd734"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "481b48894073a0096f28cbe9860af01fc1b861e55b3bc96afafc645ee3de62dc"
dependencies = [
"anyhow",
"async-dispatcher",
"async-std",
"aws-lc-rs",
"base64 0.22.1",
"bytes 1.10.1",
"chrono",
"data-encoding",
"dirs 5.0.1",
"dirs 6.0.0",
"futures 0.3.31",
"glob",
"jupyter-protocol",
"ring",
"serde",
"serde_json",
"shellexpand 3.1.1",
"smol",
"thiserror 2.0.17",
"uuid",
"zeromq",
]
@@ -14692,7 +14755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring",
"untrusted",
"untrusted 0.9.0",
]
[[package]]
@@ -14704,7 +14767,7 @@ dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted",
"untrusted 0.9.0",
]
[[package]]
@@ -14934,7 +14997,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
dependencies = [
"ring",
"untrusted",
"untrusted 0.9.0",
]
[[package]]
@@ -15837,6 +15900,15 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spin"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
dependencies = [
"lock_api",
]
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@@ -16208,7 +16280,6 @@ dependencies = [
"log",
"menu",
"picker",
"project",
"reqwest_client",
"rust-embed",
"settings",
@@ -16218,7 +16289,6 @@ dependencies = [
"theme",
"title_bar",
"ui",
"workspace",
]
[[package]]
@@ -16524,6 +16594,33 @@ dependencies = [
"zeno",
]
[[package]]
name = "sweep_ai"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
"brotli",
"client",
"collections",
"edit_prediction",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
"indoc",
"language",
"project",
"release_channel",
"reqwest_client",
"serde",
"serde_json",
"tree-sitter-rust",
"util",
"workspace",
"zlog",
]
[[package]]
name = "symphonia"
version = "0.5.5"
@@ -18520,6 +18617,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -18804,7 +18907,6 @@ dependencies = [
name = "vim_mode_setting"
version = "0.1.0"
dependencies = [
"gpui",
"settings",
]
@@ -20948,6 +21050,7 @@ dependencies = [
"gh-workflow",
"indexmap 2.11.4",
"indoc",
"serde",
"toml 0.8.23",
"toml_edit 0.22.27",
]
@@ -21132,7 +21235,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.213.0"
version = "0.215.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21145,11 +21248,11 @@ dependencies = [
"audio",
"auto_update",
"auto_update_ui",
"backtrace",
"bincode 1.3.3",
"breadcrumbs",
"call",
"channel",
"chrono",
"clap",
"cli",
"client",
@@ -21207,8 +21310,8 @@ dependencies = [
"menu",
"migrator",
"mimalloc",
"miniprofiler_ui",
"nc",
"nix 0.29.0",
"node_runtime",
"notifications",
"onboarding",
@@ -21244,13 +21347,13 @@ dependencies = [
"snippets_ui",
"supermaven",
"svg_preview",
"sweep_ai",
"sysinfo 0.37.2",
"system_specs",
"tab_switcher",
"task",
"tasks_ui",
"telemetry",
"telemetry_events",
"terminal_view",
"theme",
"theme_extension",
@@ -21671,18 +21774,21 @@ dependencies = [
"language_model",
"log",
"lsp",
"open_ai",
"pretty_assertions",
"project",
"release_channel",
"schemars 1.0.4",
"serde",
"serde_json",
"settings",
"smol",
"strsim",
"thiserror 2.0.17",
"util",
"uuid",
"workspace",
"worktree",
"zlog",
]
[[package]]
@@ -21694,6 +21800,7 @@ dependencies = [
"clap",
"client",
"cloud_llm_client",
"cloud_zeta2_prompt",
"collections",
"edit_prediction_context",
"editor",
@@ -21707,7 +21814,6 @@ dependencies = [
"ordered-float 2.10.1",
"pretty_assertions",
"project",
"regex-syntax",
"serde",
"serde_json",
"settings",

View File

@@ -110,6 +110,7 @@ members = [
"crates/menu",
"crates/migrator",
"crates/mistral",
"crates/miniprofiler_ui",
"crates/multi_buffer",
"crates/nc",
"crates/net",
@@ -126,6 +127,7 @@ members = [
"crates/picker",
"crates/prettier",
"crates/project",
"crates/project_benchmarks",
"crates/project_panel",
"crates/project_symbols",
"crates/prompt_store",
@@ -163,6 +165,7 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/sweep_ai",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
@@ -341,6 +344,7 @@ menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
mistral = { path = "crates/mistral" }
multi_buffer = { path = "crates/multi_buffer" }
miniprofiler_ui = { path = "crates/miniprofiler_ui" }
nc = { path = "crates/nc" }
net = { path = "crates/net" }
node_runtime = { path = "crates/node_runtime" }
@@ -395,6 +399,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
sweep_ai = { path = "crates/sweep_ai" }
codestral = { path = "crates/codestral" }
system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
@@ -440,7 +445,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { path = "../agent-client-protocol", features = ["unstable"] }
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
@@ -458,7 +463,7 @@ async-tar = "0.5.1"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.31.0"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
aws-config = { version = "1.6.1", features = ["behavior-version-latest"] }
aws-credential-types = { version = "1.2.2", features = [
"hardcoded-credentials",
@@ -475,6 +480,7 @@ bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
brotli = "8.0.2"
bytes = "1.0"
cargo_metadata = "0.19"
cargo_toml = "0.21"
@@ -482,7 +488,7 @@ cfg-if = "1.0.3"
chrono = { version = "0.4", features = ["serde"] }
ciborium = "0.2"
circular-buffer = "1.0"
clap = { version = "4.4", features = ["derive"] }
clap = { version = "4.4", features = ["derive", "wrap_help"] }
cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0"
convert_case = "0.8.0"
@@ -504,7 +510,7 @@ emojis = "0.6.1"
env_logger = "0.11"
exec = "0.3.1"
fancy-regex = "0.14.0"
fork = "0.2.0"
fork = "0.4.0"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
@@ -531,8 +537,8 @@ itertools = "0.14.0"
json_dotpath = "1.1"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-protocol = "0.10.0"
jupyter-websocket-client = "0.15.0"
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
@@ -545,7 +551,7 @@ minidumper = "0.8"
moka = { version = "0.12.10", features = ["sync"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nbformat = "0.15.0"
nix = "0.29"
num-format = "0.4.4"
num-traits = "0.2"
@@ -616,8 +622,8 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662
"stream",
], package = "zed-reqwest", version = "0.12.15-zed" }
rsa = "0.9.6"
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
"async-dispatcher-runtime",
runtimelib = { version = "0.30.0", default-features = false, features = [
"async-dispatcher-runtime", "aws-lc-rs"
] }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
@@ -628,6 +634,7 @@ scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_derive = "1.0.221"
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
@@ -663,6 +670,7 @@ time = { version = "0.3", features = [
"serde",
"serde-well-known",
"formatting",
"local-offset",
] }
tiny_http = "0.8"
tokio = { version = "1" }
@@ -720,6 +728,7 @@ yawc = "0.2.5"
zeroize = "1.8"
zstd = "0.11"
[workspace.dependencies.windows]
version = "0.61"
features = [
@@ -772,8 +781,8 @@ features = [
]
[patch.crates-io]
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" }
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
[profile.dev]
@@ -788,6 +797,19 @@ codegen-units = 16
codegen-units = 16
[profile.dev.package]
# proc-macros start
gpui_macros = { opt-level = 3 }
derive_refineable = { opt-level = 3 }
settings_macros = { opt-level = 3 }
sqlez_macros = { opt-level = 3, codegen-units = 1 }
ui_macros = { opt-level = 3 }
util_macros = { opt-level = 3 }
serde_derive = { opt-level = 3 }
quote = { opt-level = 3 }
syn = { opt-level = 3 }
proc-macro2 = { opt-level = 3 }
# proc-macros end
taffy = { opt-level = 3 }
cranelift-codegen = { opt-level = 3 }
cranelift-codegen-meta = { opt-level = 3 }
@@ -829,7 +851,6 @@ semantic_version = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }
sqlez_macros = { codegen-units = 1 }
story = { codegen-units = 1 }
supermaven_api = { codegen-units = 1 }
telemetry_events = { codegen-units = 1 }

View File

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

View File

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

View File

@@ -44,6 +44,7 @@ design
docs
= @probably-neb
= @miguelraz
extension
= @kubkon
@@ -98,6 +99,9 @@ settings_ui
= @danilo-leal
= @probably-neb
support
= @miguelraz
tasks
= @SomeoneToIgnore
= @Veykril

4
assets/icons/at_sign.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00156 10.3996C9.32705 10.3996 10.4016 9.32509 10.4016 7.99961C10.4016 6.67413 9.32705 5.59961 8.00156 5.59961C6.67608 5.59961 5.60156 6.67413 5.60156 7.99961C5.60156 9.32509 6.67608 10.3996 8.00156 10.3996Z" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.4 5.6V8.6C10.4 9.07739 10.5896 9.53523 10.9272 9.8728C11.2648 10.2104 11.7226 10.4 12.2 10.4C12.6774 10.4 13.1352 10.2104 13.4728 9.8728C13.8104 9.53523 14 9.07739 14 8.6V8C14 6.64839 13.5436 5.33636 12.7048 4.27651C11.8661 3.21665 10.694 2.47105 9.37852 2.16051C8.06306 1.84997 6.68129 1.99269 5.45707 2.56554C4.23285 3.13838 3.23791 4.1078 2.63344 5.31672C2.02898 6.52565 1.85041 7.90325 2.12667 9.22633C2.40292 10.5494 3.11782 11.7405 4.15552 12.6065C5.19323 13.4726 6.49295 13.9629 7.84411 13.998C9.19527 14.0331 10.5187 13.611 11.6 12.8" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

32
assets/icons/sweep_ai.svg Normal file
View File

@@ -0,0 +1,32 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3348_16)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
<mask id="mask0_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="4" y="5" width="9" height="7">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.97419 6.27207C8.44653 6.29114 8.86622 6.27046 9.23628 6.22425C9.08884 7.48378 8.7346 8.72903 8.16697 9.90688C8.04459 9.83861 7.92582 9.76008 7.81193 9.67108C7.64539 9.54099 7.49799 9.39549 7.37015 9.23818C7.5282 9.54496 7.64901 9.86752 7.73175 10.1986C7.35693 10.6656 6.90663 11.0373 6.412 11.3101C5.01165 10.8075 4.03638 9.63089 4.03638 7.93001C4.03638 6.96185 4.35234 6.07053 4.88281 5.36157C5.34001 5.69449 6.30374 6.20455 7.97419 6.27207ZM8.27511 11.5815C10.3762 11.5349 11.8115 10.7826 12.8347 7.93001C11.6992 7.93001 11.4246 7.10731 11.1188 6.19149C11.0669 6.03596 11.0141 5.87771 10.956 5.72037C10.6733 5.86733 10.2753 6.02782 9.74834 6.13895C9.59658 7.49345 9.20592 8.83238 8.56821 10.0897C8.89933 10.2093 9.24674 10.262 9.5908 10.2502C9.08928 10.4803 8.62468 10.8066 8.22655 11.2255C8.2457 11.3438 8.26186 11.4625 8.27511 11.5815ZM6.62702 7.75422C6.62702 7.50604 6.82821 7.30485 7.07639 7.30485C7.32457 7.30485 7.52576 7.50604 7.52576 7.75422V8.23616C7.52576 8.48435 7.32457 8.68554 7.07639 8.68554C6.82821 8.68554 6.62702 8.48435 6.62702 8.23616V7.75422ZM5.27746 7.30485C5.05086 7.30485 4.86716 7.48854 4.86716 7.71513V8.27525C4.86716 8.50185 5.05086 8.68554 5.27746 8.68554C5.50406 8.68554 5.68776 8.50185 5.68776 8.27525V7.71513C5.68776 7.48854 5.50406 7.30485 5.27746 7.30485Z" fill="white"/>
</mask>
<g mask="url(#mask0_3348_16)">
<path d="M9.23617 6.22425L9.39588 6.24293L9.41971 6.0393L9.21624 6.06471L9.23617 6.22425ZM8.16687 9.90688L8.08857 10.0473L8.23765 10.1305L8.31174 9.97669L8.16687 9.90688ZM7.37005 9.23819L7.49487 9.13676L7.22714 9.3118L7.37005 9.23819ZM7.73165 10.1986L7.85702 10.2993L7.90696 10.2371L7.88761 10.1597L7.73165 10.1986ZM6.41189 11.3101L6.35758 11.4615L6.42594 11.486L6.48954 11.4509L6.41189 11.3101ZM4.88271 5.36157L4.97736 5.23159L4.84905 5.13817L4.75397 5.26525L4.88271 5.36157ZM8.27501 11.5815L8.11523 11.5993L8.13151 11.7456L8.27859 11.7423L8.27501 11.5815ZM12.8346 7.93001L12.986 7.98428L13.0631 7.76921H12.8346V7.93001ZM10.9559 5.72037L11.1067 5.66469L11.0436 5.49354L10.8817 5.5777L10.9559 5.72037ZM9.74824 6.13896L9.71508 5.98161L9.60139 6.0056L9.58846 6.12102L9.74824 6.13896ZM8.56811 10.0897L8.42469 10.017L8.34242 10.1792L8.51348 10.241L8.56811 10.0897ZM9.5907 10.2502L9.65775 10.3964L9.58519 10.0896L9.5907 10.2502ZM8.22644 11.2255L8.10992 11.1147L8.05502 11.1725L8.06773 11.2512L8.22644 11.2255ZM9.21624 6.06471C8.85519 6.10978 8.44439 6.13015 7.98058 6.11139L7.96756 6.43272C8.44852 6.45215 8.87701 6.43111 9.25607 6.3838L9.21624 6.06471ZM8.31174 9.97669C8.88724 8.78244 9.2464 7.51988 9.39588 6.24293L9.07647 6.20557C8.93108 7.44772 8.58175 8.67563 8.02203 9.83708L8.31174 9.97669ZM8.2452 9.76645C8.12998 9.70219 8.01817 9.62826 7.91082 9.54438L7.71285 9.79779C7.8333 9.8919 7.95895 9.97503 8.08857 10.0473L8.2452 9.76645ZM7.91082 9.54438C7.75387 9.4218 7.61512 9.28479 7.49487 9.13676L7.24526 9.33957C7.38066 9.50619 7.53671 9.66023 7.71285 9.79779L7.91082 9.54438ZM7.22714 9.3118C7.37944 9.60746 7.49589 9.91837 7.57564 10.2376L7.88761 10.1597C7.80196 9.81663 7.67679 9.48248 7.513 9.16453L7.22714 9.3118ZM7.60624 10.098C7.24483 10.5482 6.81083 10.9065 6.33425 11.1693L6.48954 11.4509C7.00223 11.1682 7.46887 10.7829 7.85702 10.2993L7.60624 10.098ZM3.87549 7.93001C3.87548 9.7042 4.89861 10.9378 6.35758 11.4615L6.46622 11.1588C5.12449 10.6772 4.19707 9.55763 4.19707 7.93001H3.87549ZM4.75397 5.26525C4.20309 6.00147 3.87549 6.92646 3.87549 7.93001H4.19707C4.19707 6.99724 4.50139 6.13959 5.01145 5.45791L4.75397 5.26525ZM7.98058 6.11139C6.34236 6.04516 5.40922 5.54604 4.97736 5.23159L4.78806 5.49157C5.27058 5.84291 6.26491 6.3639 7.96756 6.43272L7.98058 6.11139ZM8.27859 11.7423C9.34696 11.7185 10.2682 11.515 11.0542 10.9376C11.8388 10.3612 12.4683 9.4273 12.986 7.98428L12.6833 7.8757C12.1776 9.28534 11.5779 10.1539 10.8638 10.6784C10.1511 11.202 9.30417 11.3978 8.27143 11.4208L8.27859 11.7423ZM12.8346 7.76921C12.3148 7.76921 12.0098 7.58516 11.7925 7.30552C11.5639 7.0114 11.4266 6.60587 11.2712 6.14061L10.9662 6.24242C11.1166 6.69294 11.2695 7.15667 11.5385 7.50285C11.8188 7.86347 12.2189 8.09078 12.8346 8.09078V7.76921ZM11.2712 6.14061C11.2195 5.98543 11.1658 5.82478 11.1067 5.66469L10.805 5.77606C10.8621 5.93065 10.9142 6.0865 10.9662 6.24242L11.2712 6.14061ZM10.8817 5.5777C10.6115 5.71821 10.2273 5.87362 9.71508 5.98161L9.78143 6.29626C10.3232 6.18206 10.735 6.0165 11.0301 5.86301L10.8817 5.5777ZM9.58846 6.12102C9.43882 7.45684 9.05355 8.77717 8.42469 10.017L8.71149 10.1625C9.35809 8.88764 9.75417 7.53011 9.90806 6.15685L9.58846 6.12102ZM9.58519 10.0896C9.26119 10.1006 8.93423 10.051 8.62269 9.93854L8.51348 10.241C8.86427 10.3677 9.23205 10.4234 9.5962 10.4109L9.58519 10.0896ZM8.34301 11.3363C8.72675 10.9325 9.17443 10.6181 9.65775 10.3964L9.52365 10.1041C9.00392 10.3425 8.52241 10.6807 8.10992 11.1147L8.34301 11.3363ZM8.43483 11.5638C8.4213 11.4421 8.40475 11.3207 8.3852 11.1998L8.06773 11.2512C8.08644 11.3668 8.10225 11.4829 8.11523 11.5993L8.43483 11.5638ZM7.07629 7.14405C6.73931 7.14405 6.46613 7.41724 6.46613 7.75423H6.7877C6.7877 7.59484 6.91691 7.46561 7.07629 7.46561V7.14405ZM7.68646 7.75423C7.68646 7.41724 7.41326 7.14405 7.07629 7.14405V7.46561C7.23567 7.46561 7.36489 7.59484 7.36489 7.75423H7.68646ZM7.68646 8.23616V7.75423H7.36489V8.23616H7.68646ZM7.07629 8.84634C7.41326 8.84634 7.68646 8.57315 7.68646 8.23616H7.36489C7.36489 8.39555 7.23567 8.52474 7.07629 8.52474V8.84634ZM6.46613 8.23616C6.46613 8.57315 6.73931 8.84634 7.07629 8.84634V8.52474C6.91691 8.52474 6.7877 8.39555 6.7877 8.23616H6.46613ZM6.46613 7.75423V8.23616H6.7877V7.75423H6.46613ZM5.02785 7.71514C5.02785 7.57734 5.13956 7.46561 5.27736 7.46561V7.14405C4.96196 7.14405 4.70627 7.39974 4.70627 7.71514H5.02785ZM5.02785 8.27525V7.71514H4.70627V8.27525H5.02785ZM5.27736 8.52474C5.13956 8.52474 5.02785 8.41305 5.02785 8.27525H4.70627C4.70627 8.59065 4.96196 8.84634 5.27736 8.84634V8.52474ZM5.52687 8.27525C5.52687 8.41305 5.41516 8.52474 5.27736 8.52474V8.84634C5.59277 8.84634 5.84845 8.59065 5.84845 8.27525H5.52687ZM5.52687 7.71514V8.27525H5.84845V7.71514H5.52687ZM5.27736 7.46561C5.41516 7.46561 5.52687 7.57734 5.52687 7.71514H5.84845C5.84845 7.39974 5.59277 7.14405 5.27736 7.14405V7.46561Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.12635 14.5901C7.22369 14.3749 7.3069 14.1501 7.37454 13.9167C7.54132 13.3412 7.5998 12.7599 7.56197 12.1948C7.53665 12.5349 7.47589 12.8775 7.37718 13.2181C7.23926 13.694 7.03667 14.1336 6.78174 14.5301C6.89605 14.5547 7.01101 14.5747 7.12635 14.5901Z" fill="white"/>
<path d="M9.71984 7.74796C9.50296 7.74796 9.29496 7.83412 9.14159 7.98745C8.98822 8.14082 8.9021 8.34882 8.9021 8.5657C8.9021 8.78258 8.98822 8.99057 9.14159 9.14394C9.29496 9.29728 9.50296 9.38344 9.71984 9.38344V8.5657V7.74796Z" fill="white"/>
<mask id="mask1_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="5" y="2" width="8" height="9">
<path d="M12.3783 2.9985H5.36792V10.3954H12.3783V2.9985Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.75733 3.61999C9.98577 5.80374 9.60089 8.05373 8.56819 10.0898C8.43122 10.0403 8.29704 9.9794 8.16699 9.90688C9.15325 7.86033 9.49538 5.61026 9.22757 3.43526C9.39923 3.51584 9.57682 3.57729 9.75733 3.61999Z" fill="black"/>
</mask>
<g mask="url(#mask1_3348_16)">
<path d="M8.56815 10.0898L8.67689 10.1449L8.62812 10.241L8.52678 10.2044L8.56815 10.0898ZM9.75728 3.61998L9.78536 3.50136L9.86952 3.52127L9.87853 3.6073L9.75728 3.61998ZM8.16695 9.90687L8.1076 10.0133L8.00732 9.9574L8.05715 9.85398L8.16695 9.90687ZM9.22753 3.43524L9.10656 3.45014L9.07958 3.23116L9.27932 3.32491L9.22753 3.43524ZM8.45945 10.0346C9.48122 8.02009 9.86217 5.79374 9.63608 3.63266L9.87853 3.6073C10.1093 5.81372 9.72048 8.0873 8.67689 10.1449L8.45945 10.0346ZM8.22633 9.80041C8.35056 9.86971 8.47876 9.92791 8.60956 9.97514L8.52678 10.2044C8.38363 10.1527 8.24344 10.0891 8.1076 10.0133L8.22633 9.80041ZM9.34849 3.42035C9.61905 5.61792 9.27346 7.89158 8.27675 9.9598L8.05715 9.85398C9.03298 7.82905 9.37158 5.60258 9.10656 3.45014L9.34849 3.42035ZM9.72925 3.7386C9.54064 3.69399 9.3551 3.62977 9.17573 3.54558L9.27932 3.32491C9.44327 3.40188 9.61288 3.46058 9.78536 3.50136L9.72925 3.7386Z" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4118 3.46925L11.2416 3.39926L11.1904 3.57611L11.349 3.62202C11.1904 3.57611 11.1904 3.57615 11.1904 3.5762L11.1903 3.57631L11.1902 3.57658L11.19 3.57741L11.1893 3.58009C11.1886 3.58233 11.1878 3.58548 11.1867 3.58949C11.1845 3.5975 11.1814 3.60897 11.1777 3.62359C11.1703 3.6528 11.1603 3.69464 11.1493 3.74656C11.1275 3.85017 11.102 3.99505 11.0869 4.16045C11.0573 4.4847 11.0653 4.91594 11.2489 5.26595C11.2613 5.28944 11.2643 5.31174 11.2625 5.32629C11.261 5.33849 11.2572 5.34226 11.2536 5.3449C11.0412 5.50026 10.5639 5.78997 9.76653 5.96607C9.76095 6.02373 9.75493 6.08134 9.74848 6.13895C10.601 5.95915 11.1161 5.65017 11.3511 5.4782C11.4413 5.41219 11.4471 5.28823 11.3952 5.18922C11.1546 4.73063 11.2477 4.08248 11.3103 3.78401C11.3314 3.68298 11.349 3.62202 11.349 3.62202C11.3745 3.6325 11.4002 3.63983 11.4259 3.64425C11.9083 3.72709 12.4185 2.78249 12.6294 2.33939C12.6852 2.22212 12.6234 2.08843 12.497 2.05837C11.2595 1.76399 5.46936 0.631807 4.57214 4.96989C4.55907 5.03307 4.57607 5.10106 4.62251 5.14584C4.87914 5.39322 5.86138 6.18665 7.9743 6.27207C8.44664 6.29114 8.86633 6.27046 9.23638 6.22425C9.24295 6.16797 9.24912 6.1117 9.25491 6.05534C8.88438 6.10391 8.46092 6.12641 7.98094 6.10702C5.91152 6.02337 4.96693 5.24843 4.73714 5.02692C4.73701 5.02679 4.73545 5.02525 4.73422 5.0208C4.73292 5.01611 4.73254 5.00987 4.73388 5.00334C4.94996 3.95861 5.4573 3.25195 6.11188 2.77714C6.77039 2.29947 7.58745 2.04983 8.42824 1.94075C10.1122 1.72228 11.8454 2.07312 12.4588 2.21906C12.4722 2.22225 12.4787 2.22927 12.4819 2.2362C12.4853 2.24342 12.4869 2.25443 12.4803 2.2684C12.3706 2.49879 12.183 2.85746 11.9656 3.13057C11.8564 3.26783 11.7479 3.37295 11.6469 3.43216C11.5491 3.48956 11.4752 3.49529 11.4118 3.46925Z" fill="white"/>
<mask id="mask2_3348_16" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="9" width="7" height="6">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.22654 11.2255C8.62463 10.8066 9.08923 10.4803 9.59075 10.2502C8.97039 10.2715 8.33933 10.0831 7.81189 9.67109C7.64534 9.541 7.49795 9.39549 7.37014 9.23819C7.52815 9.54497 7.64896 9.86752 7.7317 10.1986C6.70151 11.4821 5.1007 12.0466 3.57739 11.8125C3.85909 12.527 4.32941 13.178 4.97849 13.6851C5.8625 14.3756 6.92544 14.6799 7.96392 14.6227C8.32513 13.5174 8.4085 12.351 8.22654 11.2255Z" fill="white"/>
</mask>
<g mask="url(#mask2_3348_16)">
<path d="M9.59085 10.2502L9.58389 10.0472L9.67556 10.4349L9.59085 10.2502ZM8.22663 11.2255L8.02607 11.258L8.00999 11.1585L8.07936 11.0856L8.22663 11.2255ZM7.37024 9.23819L7.18961 9.33119L7.52789 9.11006L7.37024 9.23819ZM7.7318 10.1986L7.92886 10.1494L7.95328 10.2472L7.8902 10.3258L7.7318 10.1986ZM3.57749 11.8125L3.3885 11.887L3.25879 11.5579L3.60835 11.6117L3.57749 11.8125ZM7.96402 14.6227L8.15711 14.6858L8.11397 14.8179L7.97519 14.8255L7.96402 14.6227ZM9.67556 10.4349C9.19708 10.6544 8.7538 10.9657 8.37387 11.3655L8.07936 11.0856C8.49566 10.6475 8.98161 10.3062 9.50614 10.0656L9.67556 10.4349ZM7.93704 9.51099C8.42551 9.89261 9.00942 10.0669 9.58389 10.0472L9.59781 10.4533C8.93151 10.4761 8.25334 10.2737 7.68693 9.83118L7.93704 9.51099ZM7.52789 9.11006C7.64615 9.25565 7.78261 9.39038 7.93704 9.51099L7.68693 9.83118C7.50827 9.69161 7.34994 9.53537 7.21254 9.36627L7.52789 9.11006ZM7.5347 10.2479C7.45573 9.93178 7.34043 9.62393 7.18961 9.33119L7.55082 9.14514C7.71611 9.466 7.84242 9.80326 7.92886 10.1494L7.5347 10.2479ZM3.60835 11.6117C5.06278 11.8352 6.59038 11.2962 7.57335 10.0715L7.8902 10.3258C6.81284 11.6681 5.1388 12.258 3.54663 12.0133L3.60835 11.6117ZM4.85352 13.8452C4.17512 13.3152 3.68312 12.6343 3.3885 11.887L3.76648 11.738C4.03524 12.4197 4.4839 13.0409 5.10364 13.525L4.85352 13.8452ZM7.97519 14.8255C6.8895 14.8853 5.77774 14.5672 4.85352 13.8452L5.10364 13.525C5.94745 14.1842 6.96157 14.4744 7.95285 14.4198L7.97519 14.8255ZM8.42716 11.1931C8.61419 12.3499 8.52858 13.5491 8.15711 14.6858L7.77093 14.5596C8.12191 13.4857 8.20296 12.352 8.02607 11.258L8.42716 11.1931Z" fill="white"/>
</g>
</g>
<defs>
<clipPath id="clip0_3348_16">
<rect width="9.63483" height="14" fill="white" transform="translate(3.19995 1.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -735,6 +735,20 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet && has_next_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::NextSnippetTabstop"
}
},
{
"context": "Editor && in_snippet && has_previous_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"shift-tab": "editor::PreviousSnippetTabstop"
}
},
// Bindings for accepting edit predictions
//
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is

View File

@@ -313,7 +313,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-n": "agent::NewTextThread",
"cmd-alt-t": "agent::NewThread"
"cmd-alt-n": "agent::NewExternalAgentThread"
}
},
{
@@ -805,6 +805,20 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet && has_next_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::NextSnippetTabstop"
}
},
{
"context": "Editor && in_snippet && has_previous_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"shift-tab": "editor::PreviousSnippetTabstop"
}
},
{
"context": "Editor && edit_prediction",
"bindings": {

View File

@@ -739,6 +739,20 @@
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && in_snippet && has_next_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"tab": "editor::NextSnippetTabstop"
}
},
{
"context": "Editor && in_snippet && has_previous_tabstop && !showing_completions",
"use_key_equivalents": true,
"bindings": {
"shift-tab": "editor::PreviousSnippetTabstop"
}
},
// Bindings for accepting edit predictions
//
// alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is

View File

@@ -421,12 +421,6 @@
"ctrl-[": "editor::Cancel"
}
},
{
"context": "vim_mode == helix_select && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {
@@ -455,6 +449,7 @@
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"d": "vim::HelixDelete",
"alt-d": "editor::Delete", // Delete selection, without yanking
"c": "vim::HelixSubstitute",
"alt-c": "vim::HelixSubstituteNoYank",

View File

@@ -605,6 +605,10 @@
// to both the horizontal and vertical delta values while scrolling. Fast scrolling
// happens when a user holds the alt or option key while scrolling.
"fast_scroll_sensitivity": 4.0,
"sticky_scroll": {
// Whether to stick scopes to the top of the editor.
"enabled": false
},
"relative_line_numbers": "disabled",
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
"search_wrap": true,
@@ -612,9 +616,13 @@
"search": {
// Whether to show the project search button in the status bar.
"button": true,
// Whether to only match on whole words.
"whole_word": false,
// Whether to match case sensitively.
"case_sensitive": false,
// Whether to include gitignored files in search results.
"include_ignored": false,
// Whether to interpret the search query as a regular expression.
"regex": false,
// Whether to center the cursor on each search match when navigating.
"center_on_match": false
@@ -734,14 +742,31 @@
// "never"
"show": "always"
},
// Sort order for entries in the project panel.
// This setting can take three values:
//
// 1. Show directories first, then files:
// "directories_first"
// 2. Mix directories and files together:
// "mixed"
// 3. Show files first, then directories:
// "files_first"
"sort_mode": "directories_first",
// 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,
// Whether to hide the hidden entries in the project panel.
"hide_hidden": false,
// Whether to automatically open files when pasting them in the project panel.
"open_file_on_paste": true
// Settings for automatically opening files.
"auto_open": {
// Whether to automatically open newly created files in the editor.
"on_create": true,
// Whether to automatically open files after pasting or duplicating them.
"on_paste": true,
// Whether to automatically open files dropped from external sources.
"on_drop": true
}
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
@@ -1487,7 +1512,11 @@
// in your project's settings, rather than globally.
"directories": [".env", "env", ".venv", "venv"],
// Can also be `csh`, `fish`, `nushell` and `power_shell`
"activate_script": "default"
"activate_script": "default",
// Preferred Conda manager to use when activating Conda environments.
// Values: "auto", "conda", "mamba", "micromamba"
// Default: "auto"
"conda_manager": "auto"
}
},
"toolbar": {
@@ -1531,6 +1560,8 @@
// Default: 10_000, maximum: 100_000 (all bigger values set will be treated as 100_000), 0 disables the scrolling.
// Existing terminals will not pick up this change until they are recreated.
"max_scroll_history_lines": 10000,
// The multiplier for scrolling speed in the terminal.
"scroll_multiplier": 1.0,
// 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.
@@ -2028,6 +2059,18 @@
"dev": {
// "theme": "Andromeda"
},
// Settings overrides to use when using linux
"linux": {},
// Settings overrides to use when using macos
"macos": {},
// Settings overrides to use when using windows
"windows": {
"languages": {
"PHP": {
"language_servers": ["intelephense", "!phpactor", "..."]
}
}
},
// Whether to show full labels in line indicator or short ones
//
// Values:

View File

@@ -39,6 +39,7 @@ serde_json.workspace = true
settings.workspace = true
smol.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
ui.workspace = true
url.workspace = true
@@ -56,3 +57,4 @@ rand.workspace = true
tempfile.workspace = true
util.workspace = true
settings.workspace = true
zlog.workspace = true

View File

@@ -15,7 +15,7 @@ use settings::Settings as _;
use task::{Shell, ShellBuilder};
pub use terminal::*;
use action_log::ActionLog;
use action_log::{ActionLog, ActionLogTelemetry};
use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result, anyhow};
use editor::Bias;
@@ -38,10 +38,10 @@ use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle
use uuid::Uuid;
#[derive(Debug)]
pub struct UserMessage<T> {
pub struct UserMessage {
pub id: Option<UserMessageId>,
pub content: ContentBlock,
pub chunks: Vec<acp::ContentBlock<T>>,
pub chunks: Vec<acp::ContentBlock>,
pub checkpoint: Option<Checkpoint>,
}
@@ -51,7 +51,7 @@ pub struct Checkpoint {
pub show: bool,
}
impl<T> UserMessage<T> {
impl UserMessage {
fn to_markdown(&self, cx: &App) -> String {
let mut markdown = String::new();
if self
@@ -116,13 +116,13 @@ impl AssistantMessageChunk {
}
#[derive(Debug)]
pub enum AgentThreadEntry<T> {
UserMessage(UserMessage<T>),
pub enum AgentThreadEntry {
UserMessage(UserMessage),
AssistantMessage(AssistantMessage),
ToolCall(ToolCall),
}
impl<T> AgentThreadEntry<T> {
impl AgentThreadEntry {
pub fn to_markdown(&self, cx: &App) -> String {
match self {
Self::UserMessage(message) => message.to_markdown(cx),
@@ -131,7 +131,7 @@ impl<T> AgentThreadEntry<T> {
}
}
pub fn user_message(&self) -> Option<&UserMessage<T>> {
pub fn user_message(&self) -> Option<&UserMessage> {
if let AgentThreadEntry::UserMessage(message) = self {
Some(message)
} else {
@@ -802,11 +802,9 @@ pub struct RetryStatus {
pub duration: Duration,
}
pub struct AnchoredText;
pub struct AcpThread<T = SharedString> {
title: T,
entries: Vec<AgentThreadEntry<AnchoredText>>,
pub struct AcpThread {
title: SharedString,
entries: Vec<AgentThreadEntry>,
plan: Plan,
project: Entity<Project>,
action_log: Entity<ActionLog>,
@@ -822,6 +820,15 @@ pub struct AcpThread<T = SharedString> {
pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>,
}
impl From<&AcpThread> for ActionLogTelemetry {
fn from(value: &AcpThread) -> Self {
Self {
agent_telemetry_id: value.connection().telemetry_id(),
session_id: value.session_id.0.clone(),
}
}
}
#[derive(Debug)]
pub enum AcpThreadEvent {
NewEntry,
@@ -1004,7 +1011,7 @@ impl Display for LoadError {
impl Error for LoadError {}
impl<T> AcpThread<T> {
impl AcpThread {
pub fn new(
title: impl Into<SharedString>,
connection: Rc<dyn AgentConnection>,
@@ -1154,7 +1161,7 @@ impl<T> AcpThread<T> {
pub fn push_user_content_block(
&mut self,
message_id: Option<UserMessageId>,
chunk: acp::ContentBlock<T>,
chunk: acp::ContentBlock,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
@@ -1233,7 +1240,7 @@ impl<T> AcpThread<T> {
}
}
fn push_entry(&mut self, entry: AgentThreadEntry<T>, cx: &mut Context<Self>) {
fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
self.entries.push(entry);
cx.emit(AcpThreadEvent::NewEntry);
}
@@ -1348,6 +1355,17 @@ impl<T> AcpThread<T> {
let path_style = self.project.read(cx).path_style(cx);
let id = update.id.clone();
let agent = self.connection().telemetry_id();
let session = self.session_id();
if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
let status = if matches!(status, ToolCallStatus::Completed) {
"completed"
} else {
"failed"
};
telemetry::event!("Agent Tool Call Completed", agent, session, status);
}
if let Some(ix) = self.index_for_tool_call(&id) {
let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
unreachable!()
@@ -1848,10 +1866,14 @@ impl<T> AcpThread<T> {
.checkpoint
.as_ref()
.map(|c| c.git_checkpoint.clone());
// Cancel any in-progress generation before restoring
let cancel_task = self.cancel(cx);
let rewind = self.rewind(id.clone(), cx);
let git_store = self.project.read(cx).git_store().clone();
cx.spawn(async move |_, cx| {
cancel_task.await;
rewind.await?;
if let Some(checkpoint) = checkpoint {
git_store
@@ -1871,16 +1893,34 @@ impl<T> AcpThread<T> {
return Task::ready(Err(anyhow!("not supported")));
};
let telemetry = ActionLogTelemetry::from(&*self);
cx.spawn(async move |this, cx| {
cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
this.update(cx, |this, cx| {
if let Some((ix, _)) = this.user_message_mut(&id) {
// Collect all terminals from entries that will be removed
let terminals_to_remove: Vec<acp::TerminalId> = this.entries[ix..]
.iter()
.flat_map(|entry| entry.terminals())
.filter_map(|terminal| terminal.read(cx).id().clone().into())
.collect();
let range = ix..this.entries.len();
this.entries.truncate(ix);
cx.emit(AcpThreadEvent::EntriesRemoved(range));
// Kill and remove the terminals
for terminal_id in terminals_to_remove {
if let Some(terminal) = this.terminals.remove(&terminal_id) {
terminal.update(cx, |terminal, cx| {
terminal.kill(cx);
});
}
}
}
this.action_log()
.update(cx, |action_log, cx| action_log.reject_all_edits(cx))
this.action_log().update(cx, |action_log, cx| {
action_log.reject_all_edits(Some(telemetry), cx)
})
})?
.await;
Ok(())
@@ -1926,7 +1966,7 @@ impl<T> AcpThread<T> {
})
}
fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage<T>)> {
fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> {
self.entries
.iter_mut()
.enumerate()
@@ -2357,8 +2397,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
});
}
@@ -3616,6 +3654,10 @@ mod tests {
}
impl AgentConnection for FakeAgentConnection {
fn telemetry_id(&self) -> &'static str {
"fake"
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&self.auth_methods
}
@@ -3781,4 +3823,314 @@ mod tests {
}
});
}
/// Tests that restoring a checkpoint properly cleans up terminals that were
/// created after that checkpoint, and cancels any in-progress generation.
///
/// Reproduces issue #35142: When a checkpoint is restored, any terminal processes
/// that were started after that checkpoint should be terminated, and any in-progress
/// AI generation should be canceled.
#[gpui::test]
async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let connection = Rc::new(FakeAgentConnection::new());
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
// Send first user message to create a checkpoint
cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(vec!["first message".into()], cx)
})
})
.await
.unwrap();
// Send second message (creates another checkpoint) - we'll restore to this one
cx.update(|cx| {
thread.update(cx, |thread, cx| {
thread.send(vec!["second message".into()], cx)
})
})
.await
.unwrap();
// Create 2 terminals BEFORE the checkpoint that have completed running
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_1 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id_1.clone(),
label: "echo 'first'".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal_1.clone(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id_1.clone(),
data: b"first\n".to_vec(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_1.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_2 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id_2.clone(),
label: "echo 'second'".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal_2.clone(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id_2.clone(),
data: b"second\n".to_vec(),
},
cx,
);
});
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_2.clone(),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
// Get the second message ID to restore to
let second_message_id = thread.read_with(cx, |thread, _| {
// At this point we have:
// - Index 0: First user message (with checkpoint)
// - Index 1: Second user message (with checkpoint)
// No assistant responses because FakeAgentConnection just returns EndTurn
let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else {
panic!("expected user message at index 1");
};
message.id.clone().unwrap()
});
// Create a terminal AFTER the checkpoint we'll restore to.
// This simulates the AI agent starting a long-running terminal command.
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
::terminal::terminal_settings::AlternateScroll::On,
None,
0,
)
.unwrap();
builder.subscribe(cx)
});
// Register the terminal as created
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id: terminal_id.clone(),
label: "sleep 1000".to_string(),
cwd: Some(PathBuf::from("/test")),
output_byte_limit: None,
terminal: mock_terminal.clone(),
},
cx,
);
});
// Simulate the terminal producing output (still running)
thread.update(cx, |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data: b"terminal is running...\n".to_vec(),
},
cx,
);
});
// Create a tool call entry that references this terminal
// This represents the agent requesting a terminal command
thread.update(cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("terminal-tool-1".into()),
title: "Running command".into(),
kind: acp::ToolKind::Execute,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Terminal {
terminal_id: terminal_id.clone(),
}],
locations: vec![],
raw_input: Some(
serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
),
raw_output: None,
meta: None,
}),
cx,
)
.unwrap();
});
// Verify terminal exists and is in the thread
let terminal_exists_before =
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
assert!(
terminal_exists_before,
"Terminal should exist before checkpoint restore"
);
// Verify the terminal's underlying task is still running (not completed)
let terminal_running_before = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id).unwrap();
terminal_entity.read_with(cx, |term, _cx| {
term.output().is_none() // output is None means it's still running
})
});
assert!(
terminal_running_before,
"Terminal should be running before checkpoint restore"
);
// Verify we have the expected entries before restore
let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len());
assert!(
entry_count_before > 1,
"Should have multiple entries before restore"
);
// Restore the checkpoint to the second message.
// This should:
// 1. Cancel any in-progress generation (via the cancel() call)
// 2. Remove the terminal that was created after that point
thread
.update(cx, |thread, cx| {
thread.restore_checkpoint(second_message_id, cx)
})
.await
.unwrap();
// Verify that no send_task is in progress after restore
// (cancel() clears the send_task)
let has_send_task_after = thread.read_with(cx, |thread, _| thread.send_task.is_some());
assert!(
!has_send_task_after,
"Should not have a send_task after restore (cancel should have cleared it)"
);
// Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0)
let entry_count = thread.read_with(cx, |thread, _| thread.entries.len());
assert_eq!(
entry_count, 1,
"Should have 1 entry after restore (only the first user message)"
);
// Verify the 2 completed terminals from before the checkpoint still exist
let terminal_1_exists = thread.read_with(cx, |thread, _| {
thread.terminals.contains_key(&terminal_id_1)
});
assert!(
terminal_1_exists,
"Terminal 1 (from before checkpoint) should still exist"
);
let terminal_2_exists = thread.read_with(cx, |thread, _| {
thread.terminals.contains_key(&terminal_id_2)
});
assert!(
terminal_2_exists,
"Terminal 2 (from before checkpoint) should still exist"
);
// Verify they're still in completed state
let terminal_1_completed = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap();
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
});
assert!(terminal_1_completed, "Terminal 1 should still be completed");
let terminal_2_completed = thread.read_with(cx, |thread, _cx| {
let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap();
terminal_entity.read_with(cx, |term, _cx| term.output().is_some())
});
assert!(terminal_2_completed, "Terminal 2 should still be completed");
// Verify the running terminal (created after checkpoint) was removed
let terminal_3_exists =
thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id));
assert!(
!terminal_3_exists,
"Terminal 3 (created after checkpoint) should have been removed"
);
// Verify total count is 2 (the two from before the checkpoint)
let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len());
assert_eq!(
terminal_count, 2,
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
);
}
}

View File

@@ -20,6 +20,8 @@ impl UserMessageId {
}
pub trait AgentConnection {
fn telemetry_id(&self) -> &'static str;
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
@@ -106,9 +108,6 @@ pub trait AgentSessionSetTitle {
}
pub trait AgentTelemetry {
/// The name of the agent used for telemetry.
fn agent_name(&self) -> String;
/// A representation of the current thread state that can be serialized for
/// storage with telemetry events.
fn thread_data(
@@ -318,6 +317,10 @@ mod test_support {
}
impl AgentConnection for StubAgentConnection {
fn telemetry_id(&self) -> &'static str {
"stub"
}
fn auth_methods(&self) -> &[acp::AuthMethod] {
&[]
}

View File

@@ -20,6 +20,7 @@ futures.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
telemetry.workspace = true
text.workspace = true
util.workspace = true
watch.workspace = true

View File

@@ -3,7 +3,9 @@ use buffer_diff::BufferDiff;
use clock;
use collections::BTreeMap;
use futures::{FutureExt, StreamExt, channel::mpsc};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
@@ -31,71 +33,6 @@ impl ActionLog {
&self.project
}
pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> {
Some(self.tracked_buffers.get(buffer)?.snapshot.clone())
}
/// Return a unified diff patch with user edits made since last read or notification
pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> {
let diffs = self
.tracked_buffers
.values()
.filter_map(|tracked| {
if !tracked.may_have_unnotified_user_edits {
return None;
}
let text_with_latest_user_edits = tracked.diff_base.to_string();
let text_with_last_seen_user_edits = tracked.last_seen_base.to_string();
if text_with_latest_user_edits == text_with_last_seen_user_edits {
return None;
}
let patch = language::unified_diff(
&text_with_last_seen_user_edits,
&text_with_latest_user_edits,
);
let buffer = tracked.buffer.clone();
let file_path = buffer
.read(cx)
.file()
.map(|file| {
let mut path = file.full_path(cx).to_string_lossy().into_owned();
if file.path_style(cx).is_windows() {
path = path.replace('\\', "/");
}
path
})
.unwrap_or_else(|| format!("buffer_{}", buffer.entity_id()));
let mut result = String::new();
result.push_str(&format!("--- a/{}\n", file_path));
result.push_str(&format!("+++ b/{}\n", file_path));
result.push_str(&patch);
Some(result)
})
.collect::<Vec<_>>();
if diffs.is_empty() {
return None;
}
let unified_diff = diffs.join("\n\n");
Some(unified_diff)
}
/// Return a unified diff patch with user edits made since last read/notification
/// and mark them as notified
pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> {
let patch = self.unnotified_user_edits(cx);
self.tracked_buffers.values_mut().for_each(|tracked| {
tracked.may_have_unnotified_user_edits = false;
tracked.last_seen_base = tracked.diff_base.clone();
});
patch
}
fn track_buffer_internal(
&mut self,
buffer: Entity<Buffer>,
@@ -145,31 +82,26 @@ impl ActionLog {
let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
let (diff_update_tx, diff_update_rx) = mpsc::unbounded();
let diff_base;
let last_seen_base;
let unreviewed_edits;
if is_created {
diff_base = Rope::default();
last_seen_base = Rope::default();
unreviewed_edits = Patch::new(vec![Edit {
old: 0..1,
new: 0..text_snapshot.max_point().row + 1,
}])
} else {
diff_base = buffer.read(cx).as_rope().clone();
last_seen_base = diff_base.clone();
unreviewed_edits = Patch::default();
}
TrackedBuffer {
buffer: buffer.clone(),
diff_base,
last_seen_base,
unreviewed_edits,
snapshot: text_snapshot,
status,
version: buffer.read(cx).version(),
diff,
diff_update: diff_update_tx,
may_have_unnotified_user_edits: false,
_open_lsp_handle: open_lsp_handle,
_maintain_diff: cx.spawn({
let buffer = buffer.clone();
@@ -320,10 +252,9 @@ impl ActionLog {
let new_snapshot = buffer_snapshot.clone();
let unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
let edits = diff_snapshots(&old_snapshot, &new_snapshot);
let mut has_user_changes = false;
async move {
if let ChangeAuthor::User = author {
has_user_changes = apply_non_conflicting_edits(
apply_non_conflicting_edits(
&unreviewed_edits,
edits,
&mut base_text,
@@ -331,22 +262,13 @@ impl ActionLog {
);
}
(Arc::new(base_text.to_string()), base_text, has_user_changes)
(Arc::new(base_text.to_string()), base_text)
}
});
anyhow::Ok(rebase)
})??;
let (new_base_text, new_diff_base, has_user_changes) = rebase.await;
this.update(cx, |this, _| {
let tracked_buffer = this
.tracked_buffers
.get_mut(buffer)
.context("buffer not tracked")
.unwrap();
tracked_buffer.may_have_unnotified_user_edits |= has_user_changes;
})?;
let (new_base_text, new_diff_base) = rebase.await;
Self::update_diff(
this,
@@ -565,14 +487,17 @@ impl ActionLog {
&mut self,
buffer: Entity<Buffer>,
buffer_range: Range<impl language::ToPoint>,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>,
) {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return;
};
let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
match tracked_buffer.status {
TrackedBufferStatus::Deleted => {
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
self.tracked_buffers.remove(&buffer);
cx.notify();
}
@@ -581,7 +506,6 @@ impl ActionLog {
let buffer_range =
buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
let mut delta = 0i32;
tracked_buffer.unreviewed_edits.retain_mut(|edit| {
edit.old.start = (edit.old.start as i32 + delta) as u32;
edit.old.end = (edit.old.end as i32 + delta) as u32;
@@ -613,6 +537,7 @@ impl ActionLog {
.collect::<String>(),
);
delta += edit.new_len() as i32 - edit.old_len() as i32;
metrics.add_edit(edit);
false
}
});
@@ -624,19 +549,24 @@ impl ActionLog {
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
}
}
if let Some(telemetry) = telemetry {
telemetry_report_accepted_edits(&telemetry, metrics);
}
}
pub fn reject_edits_in_ranges(
&mut self,
buffer: Entity<Buffer>,
buffer_ranges: Vec<Range<impl language::ToPoint>>,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
return Task::ready(Ok(()));
};
match &tracked_buffer.status {
let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
let task = match &tracked_buffer.status {
TrackedBufferStatus::Created {
existing_file_content,
} => {
@@ -686,6 +616,7 @@ impl ActionLog {
}
};
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
self.tracked_buffers.remove(&buffer);
cx.notify();
task
@@ -699,6 +630,7 @@ impl ActionLog {
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
// Clear all tracked edits for this buffer and start over as if we just read it.
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
self.tracked_buffers.remove(&buffer);
self.buffer_read(buffer.clone(), cx);
cx.notify();
@@ -738,6 +670,7 @@ impl ActionLog {
}
if revert {
metrics.add_edit(edit);
let old_range = tracked_buffer
.diff_base
.point_to_offset(Point::new(edit.old.start, 0))
@@ -758,12 +691,25 @@ impl ActionLog {
self.project
.update(cx, |project, cx| project.save_buffer(buffer, cx))
}
};
if let Some(telemetry) = telemetry {
telemetry_report_rejected_edits(&telemetry, metrics);
}
task
}
pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
self.tracked_buffers
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
pub fn keep_all_edits(
&mut self,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>,
) {
self.tracked_buffers.retain(|buffer, tracked_buffer| {
let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
if let Some(telemetry) = telemetry.as_ref() {
telemetry_report_accepted_edits(telemetry, metrics);
}
match tracked_buffer.status {
TrackedBufferStatus::Deleted => false,
_ => {
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
@@ -774,13 +720,24 @@ impl ActionLog {
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
true
}
});
}
});
cx.notify();
}
pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
pub fn reject_all_edits(
&mut self,
telemetry: Option<ActionLogTelemetry>,
cx: &mut Context<Self>,
) -> Task<()> {
let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
let reject = self.reject_edits_in_ranges(
buffer,
vec![Anchor::MIN..Anchor::MAX],
telemetry.clone(),
cx,
);
async move {
reject.await.log_err();
@@ -788,8 +745,7 @@ impl ActionLog {
});
let task = futures::future::join_all(futures);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
task.await;
})
}
@@ -819,6 +775,61 @@ impl ActionLog {
}
}
#[derive(Clone)]
pub struct ActionLogTelemetry {
pub agent_telemetry_id: &'static str,
pub session_id: Arc<str>,
}
struct ActionLogMetrics {
lines_removed: u32,
lines_added: u32,
language: Option<SharedString>,
}
impl ActionLogMetrics {
fn for_buffer(buffer: &Buffer) -> Self {
Self {
language: buffer.language().map(|l| l.name().0),
lines_removed: 0,
lines_added: 0,
}
}
fn add_edits(&mut self, edits: &[Edit<u32>]) {
for edit in edits {
self.add_edit(edit);
}
}
fn add_edit(&mut self, edit: &Edit<u32>) {
self.lines_added += edit.new_len();
self.lines_removed += edit.old_len();
}
}
fn telemetry_report_accepted_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
telemetry::event!(
"Agent Edits Accepted",
agent = telemetry.agent_telemetry_id,
session = telemetry.session_id,
language = metrics.language,
lines_added = metrics.lines_added,
lines_removed = metrics.lines_removed
);
}
fn telemetry_report_rejected_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
telemetry::event!(
"Agent Edits Rejected",
agent = telemetry.agent_telemetry_id,
session = telemetry.session_id,
language = metrics.language,
lines_added = metrics.lines_added,
lines_removed = metrics.lines_removed
);
}
fn apply_non_conflicting_edits(
patch: &Patch<u32>,
edits: Vec<Edit<u32>>,
@@ -949,14 +960,12 @@ enum TrackedBufferStatus {
struct TrackedBuffer {
buffer: Entity<Buffer>,
diff_base: Rope,
last_seen_base: Rope,
unreviewed_edits: Patch<u32>,
status: TrackedBufferStatus,
version: clock::Global,
diff: Entity<BufferDiff>,
snapshot: text::BufferSnapshot,
diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>,
may_have_unnotified_user_edits: bool,
_open_lsp_handle: OpenLspBufferHandle,
_maintain_diff: Task<()>,
_subscription: Subscription,
@@ -987,7 +996,6 @@ mod tests {
use super::*;
use buffer_diff::DiffHunkStatusKind;
use gpui::TestAppContext;
use indoc::indoc;
use language::Point;
use project::{FakeFs, Fs, Project, RemoveOptions};
use rand::prelude::*;
@@ -1005,8 +1013,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
@@ -1066,7 +1072,7 @@ mod tests {
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), None, cx)
});
cx.run_until_parked();
assert_eq!(
@@ -1082,7 +1088,7 @@ mod tests {
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), None, cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1167,7 +1173,7 @@ mod tests {
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), None, cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1264,111 +1270,7 @@ mod tests {
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
}
#[gpui::test(iterations = 10)]
async fn test_user_edits_notifications(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"file": indoc! {"
abc
def
ghi
jkl
mno"}}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| project.find_project_path("dir/file", cx))
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
// Agent edits
cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx)
.unwrap()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
indoc! {"
abc
deF
GHI
jkl
mno"}
);
assert_eq!(
unreviewed_hunks(&action_log, cx),
vec![(
buffer.clone(),
vec![HunkStatus {
range: Point::new(1, 0)..Point::new(3, 0),
diff_status: DiffHunkStatusKind::Modified,
old_text: "def\nghi\n".into(),
}],
)]
);
// User edits
buffer.update(cx, |buffer, cx| {
buffer.edit(
[
(Point::new(0, 2)..Point::new(0, 2), "X"),
(Point::new(3, 0)..Point::new(3, 0), "Y"),
],
None,
cx,
)
});
cx.run_until_parked();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
indoc! {"
abXc
deF
GHI
Yjkl
mno"}
);
// User edits should be stored separately from agent's
let user_edits = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
assert_eq!(
user_edits.expect("should have some user edits"),
indoc! {"
--- a/dir/file
+++ b/dir/file
@@ -1,5 +1,5 @@
-abc
+abXc
def
ghi
-jkl
+Yjkl
mno
"}
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1427,7 +1329,7 @@ mod tests {
);
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), 0..5, cx)
log.keep_edits_in_range(buffer.clone(), 0..5, None, cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1479,7 +1381,7 @@ mod tests {
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx)
})
.await
.unwrap();
@@ -1559,7 +1461,7 @@ mod tests {
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx)
})
.await
.unwrap();
@@ -1742,6 +1644,7 @@ mod tests {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(4, 0)..Point::new(4, 0)],
None,
cx,
)
})
@@ -1776,6 +1679,7 @@ mod tests {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(1, 0)],
None,
cx,
)
})
@@ -1803,6 +1707,7 @@ mod tests {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(4, 0)..Point::new(4, 0)],
None,
cx,
)
})
@@ -1877,7 +1782,7 @@ mod tests {
let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
..buffer.read(cx).anchor_before(Point::new(5, 3));
log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], None, cx)
.detach();
assert_eq!(
buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1938,6 +1843,7 @@ mod tests {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(0, 0)],
None,
cx,
)
})
@@ -1993,6 +1899,7 @@ mod tests {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(0, 11)],
None,
cx,
)
})
@@ -2055,6 +1962,7 @@ mod tests {
log.reject_edits_in_ranges(
buffer.clone(),
vec![Point::new(0, 0)..Point::new(100, 0)],
None,
cx,
)
})
@@ -2102,7 +2010,7 @@ mod tests {
// User accepts the single hunk
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx)
log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, None, cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -2123,7 +2031,7 @@ mod tests {
// User rejects the hunk
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx)
log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], None, cx)
})
.await
.unwrap();
@@ -2167,7 +2075,7 @@ mod tests {
cx.run_until_parked();
// User clicks "Accept All"
action_log.update(cx, |log, cx| log.keep_all_edits(cx));
action_log.update(cx, |log, cx| log.keep_all_edits(None, cx));
cx.run_until_parked();
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
@@ -2186,7 +2094,7 @@ mod tests {
// User clicks "Reject All"
action_log
.update(cx, |log, cx| log.reject_all_edits(cx))
.update(cx, |log, cx| log.reject_all_edits(None, cx))
.await;
cx.run_until_parked();
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
@@ -2226,7 +2134,7 @@ mod tests {
action_log.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("keeping edits in range {:?}", range);
log.keep_edits_in_range(buffer.clone(), range, cx)
log.keep_edits_in_range(buffer.clone(), range, None, cx)
});
}
25..50 => {
@@ -2234,7 +2142,7 @@ mod tests {
.update(cx, |log, cx| {
let range = buffer.read(cx).random_byte_range(0, &mut rng);
log::info!("rejecting edits in range {:?}", range);
log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
log.reject_edits_in_ranges(buffer.clone(), vec![range], None, cx)
})
.await
.unwrap();
@@ -2488,61 +2396,4 @@ mod tests {
.collect()
})
}
#[gpui::test]
async fn test_format_patch(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/dir"),
json!({"test.txt": "line 1\nline 2\nline 3\n"}),
)
.await;
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let file_path = project
.read_with(cx, |project, cx| {
project.find_project_path("dir/test.txt", cx)
})
.unwrap();
let buffer = project
.update(cx, |project, cx| project.open_buffer(file_path, cx))
.await
.unwrap();
cx.update(|cx| {
// Track the buffer and mark it as read first
action_log.update(cx, |log, cx| {
log.buffer_read(buffer.clone(), cx);
});
// Make some edits to create a patch
buffer.update(cx, |buffer, cx| {
buffer
.edit([(Point::new(1, 0)..Point::new(1, 6), "CHANGED")], None, cx)
.unwrap(); // Replace "line2" with "CHANGED"
});
});
cx.run_until_parked();
// Get the patch
let patch = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx));
// Verify the patch format contains expected unified diff elements
assert_eq!(
patch.unwrap(),
indoc! {"
--- a/dir/test.txt
+++ b/dir/test.txt
@@ -1,3 +1,3 @@
line 1
-line 2
+CHANGED
line 3
"}
);
}
}

View File

@@ -17,6 +17,7 @@ anyhow.workspace = true
auto_update.workspace = true
editor.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true

View File

@@ -51,6 +51,7 @@ pub struct ActivityIndicator {
project: Entity<Project>,
auto_updater: Option<Entity<AutoUpdater>>,
context_menu_handle: PopoverMenuHandle<ContextMenu>,
fs_jobs: Vec<fs::JobInfo>,
}
#[derive(Debug)]
@@ -99,6 +100,27 @@ impl ActivityIndicator {
})
.detach();
let fs = project.read(cx).fs().clone();
let mut job_events = fs.subscribe_to_jobs();
cx.spawn(async move |this, cx| {
while let Some(job_event) = job_events.next().await {
this.update(cx, |this: &mut ActivityIndicator, cx| {
match job_event {
fs::JobEvent::Started { info } => {
this.fs_jobs.retain(|j| j.id != info.id);
this.fs_jobs.push(info);
}
fs::JobEvent::Completed { id } => {
this.fs_jobs.retain(|j| j.id != id);
}
}
cx.notify();
})?;
}
anyhow::Ok(())
})
.detach();
cx.subscribe(
&project.read(cx).lsp_store(),
|activity_indicator, _, event, cx| {
@@ -201,7 +223,8 @@ impl ActivityIndicator {
statuses: Vec::new(),
project: project.clone(),
auto_updater,
context_menu_handle: Default::default(),
context_menu_handle: PopoverMenuHandle::default(),
fs_jobs: Vec::new(),
}
});
@@ -432,6 +455,23 @@ impl ActivityIndicator {
});
}
// Show any long-running fs command
for fs_job in &self.fs_jobs {
if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY {
return Some(Content {
icon: Some(
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.with_rotate_animation(2)
.into_any_element(),
),
message: fs_job.message.clone().into(),
on_click: None,
tooltip_message: None,
});
}
}
// Show any language server installation info.
let mut downloading = SmallVec::<[_; 3]>::new();
let mut checking_for_update = SmallVec::<[_; 3]>::new();

View File

@@ -63,7 +63,6 @@ streaming_diff.workspace = true
strsim.workspace = true
task.workspace = true
telemetry.workspace = true
terminal.workspace = true
text.workspace = true
thiserror.workspace = true
ui.workspace = true

View File

@@ -6,7 +6,6 @@ mod native_agent_server;
pub mod outline;
mod templates;
mod thread;
mod tool_schema;
mod tools;
#[cfg(test)]
@@ -134,9 +133,7 @@ impl LanguageModels {
for model in provider.provided_models(cx) {
let model_info = Self::map_language_model_to_info(&model, &provider);
let model_id = model_info.id.clone();
if !recommended_models.contains(&(model.provider_id(), model.id())) {
provider_models.push(model_info);
}
provider_models.push(model_info);
models.insert(model_id, model);
}
if !provider_models.is_empty() {
@@ -218,7 +215,7 @@ impl LanguageModels {
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
"Failed to authenticate provider: {}: {err:#}",
provider_name.0
);
}
@@ -967,6 +964,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
}
impl acp_thread::AgentConnection for NativeAgentConnection {
fn telemetry_id(&self) -> &'static str {
"zed"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
@@ -1107,10 +1108,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
}
impl acp_thread::AgentTelemetry for NativeAgentConnection {
fn agent_name(&self) -> String {
"Zed".into()
}
fn thread_data(
&self,
session_id: &acp::SessionId,
@@ -1627,9 +1624,7 @@ mod internal_tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
agent_settings::init(cx);
language::init(cx);
LanguageModelRegistry::test(cx);
});
}

View File

@@ -150,6 +150,7 @@ impl DbThread {
.unwrap_or_default(),
input: tool_use.input,
is_input_complete: true,
thought_signature: None,
},
));
}

View File

@@ -1394,7 +1394,7 @@ mod tests {
async fn init_test(cx: &mut TestAppContext) -> EditAgent {
cx.update(settings::init);
cx.update(Project::init_settings);
let project = Project::test(FakeFs::new(cx.executor()), [], cx).await;
let model = Arc::new(FakeLanguageModel::default());
let action_log = cx.new(|_| ActionLog::new(project.clone()));

View File

@@ -15,12 +15,14 @@ const SEPARATOR_MARKER: &str = "=======";
const REPLACE_MARKER: &str = ">>>>>>> REPLACE";
const SONNET_PARAMETER_INVOKE_1: &str = "</parameter>\n</invoke>";
const SONNET_PARAMETER_INVOKE_2: &str = "</parameter></invoke>";
const END_TAGS: [&str; 5] = [
const SONNET_PARAMETER_INVOKE_3: &str = "</parameter>";
const END_TAGS: [&str; 6] = [
OLD_TEXT_END_TAG,
NEW_TEXT_END_TAG,
EDITS_END_TAG,
SONNET_PARAMETER_INVOKE_1, // Remove this after switching to streaming tool call
SONNET_PARAMETER_INVOKE_1, // Remove these after switching to streaming tool call
SONNET_PARAMETER_INVOKE_2,
SONNET_PARAMETER_INVOKE_3,
];
#[derive(Debug)]
@@ -567,21 +569,29 @@ mod tests {
parse_random_chunks(
indoc! {"
<old_text>some text</old_text><new_text>updated text</parameter></invoke>
<old_text>more text</old_text><new_text>upd</parameter></new_text>
"},
&mut parser,
&mut rng
),
vec![Edit {
old_text: "some text".to_string(),
new_text: "updated text".to_string(),
line_hint: None,
},]
vec![
Edit {
old_text: "some text".to_string(),
new_text: "updated text".to_string(),
line_hint: None,
},
Edit {
old_text: "more text".to_string(),
new_text: "upd".to_string(),
line_hint: None,
},
]
);
assert_eq!(
parser.finish(),
EditParserMetrics {
tags: 2,
mismatched_tags: 1
tags: 4,
mismatched_tags: 2
}
);
}

View File

@@ -1108,6 +1108,7 @@ fn tool_use(
raw_input: serde_json::to_string_pretty(&input).unwrap(),
input: serde_json::to_value(input).unwrap(),
is_input_complete: true,
thought_signature: None,
})
}
@@ -1468,14 +1469,9 @@ impl EditAgentTest {
gpui_tokio::init(cx);
let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap());
cx.set_http_client(http_client);
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
settings::init(cx);
Project::init_settings(cx);
language::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
});

View File

@@ -88,8 +88,6 @@ mod tests {
async |fs, project, cx| {
let auth = cx.update(|cx| {
prompt_store::init(cx);
terminal::init(cx);
let registry = language_model::LanguageModelRegistry::read_global(cx);
let auth = registry
.provider(&language_model::ANTHROPIC_PROVIDER_ID)

View File

@@ -1,6 +1,6 @@
use anyhow::Result;
use gpui::{AsyncApp, Entity};
use language::{Buffer, OutlineItem, ParseStatus};
use language::{Buffer, OutlineItem};
use regex::Regex;
use std::fmt::Write;
use text::Point;
@@ -30,10 +30,9 @@ pub async fn get_buffer_content_or_outline(
if file_size > AUTO_OUTLINE_SIZE {
// For large files, use outline instead of full content
// Wait until the buffer has been fully parsed, so we can read its outline
let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?;
while *parse_status.borrow() != ParseStatus::Idle {
parse_status.changed().await?;
}
buffer
.read_with(cx, |buffer, _| buffer.parsing_idle())?
.await;
let outline_items = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
@@ -45,6 +44,25 @@ pub async fn get_buffer_content_or_outline(
.collect::<Vec<_>>()
})?;
// If no outline exists, fall back to first 1KB so the agent has some context
if outline_items.is_empty() {
let text = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let len = snapshot.len().min(1024);
let content = snapshot.text_for_range(0..len).collect::<String>();
if let Some(path) = path {
format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}")
} else {
format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}")
}
})?;
return Ok(BufferContent {
text,
is_outline: false,
});
}
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
let text = if let Some(path) = path {
@@ -141,3 +159,62 @@ fn render_entries(
entries_rendered
}
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
use project::Project;
use settings::SettingsStore;
#[gpui::test]
async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings = SettingsStore::test(cx);
cx.set_global(settings);
});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let content = "A".repeat(100 * 1024); // 100KB
let content_len = content.len();
let buffer = project
.update(cx, |project, cx| project.create_buffer(true, cx))
.await
.expect("failed to create buffer");
buffer.update(cx, |buffer, cx| buffer.set_text(content, cx));
let result = cx
.spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await })
.await
.unwrap();
// Should contain some of the actual file content
assert!(
result.text.contains("AAAAAAAAAA"),
"Result did not contain content subset"
);
// Should be marked as not an outline (it's truncated content)
assert!(
!result.is_outline,
"Large file without outline should not be marked as outline"
);
// Should be reasonably sized (much smaller than original)
assert!(
result.text.len() < 50 * 1024,
"Result size {} should be smaller than 50KB",
result.text.len()
);
// Should be significantly smaller than the original content
assert!(
result.text.len() < content_len / 10,
"Result should be much smaller than original content"
);
}
}

View File

@@ -274,6 +274,7 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -461,6 +462,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -470,6 +472,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -520,6 +523,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -554,6 +558,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -592,6 +597,7 @@ async fn test_tool_hallucination(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -621,6 +627,7 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true,
thought_signature: None,
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
@@ -731,6 +738,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(),
is_input_complete: true,
thought_signature: None,
};
let tool_result = LanguageModelToolResult {
tool_use_id: "tool_id_1".into(),
@@ -933,7 +941,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
// Test that test-1 profile (default) has echo and delay tools
thread
.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-1".into()));
thread.set_profile(AgentProfileId("test-1".into()), cx);
thread.send(UserMessageId::new(), ["test"], cx)
})
.unwrap();
@@ -953,7 +961,7 @@ async fn test_profiles(cx: &mut TestAppContext) {
// Switch to test-2 profile, and verify that it has only the infinite tool.
thread
.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test-2".into()));
thread.set_profile(AgentProfileId("test-2".into()), cx);
thread.send(UserMessageId::new(), ["test2"], cx)
})
.unwrap();
@@ -1002,8 +1010,8 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
)
.await;
cx.run_until_parked();
thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()))
thread.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test".into()), cx)
});
let mut mcp_tool_calls = setup_context_server(
@@ -1037,6 +1045,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -1080,6 +1089,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "mcp"}).to_string(),
input: json!({"text": "mcp"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1089,6 +1099,7 @@ async fn test_mcp_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "native"}).to_string(),
input: json!({"text": "native"}),
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -1169,8 +1180,8 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) {
.await;
cx.run_until_parked();
thread.update(cx, |thread, _| {
thread.set_profile(AgentProfileId("test".into()));
thread.update(cx, |thread, cx| {
thread.set_profile(AgentProfileId("test".into()), cx);
thread.add_tool(EchoTool);
thread.add_tool(DelayTool);
thread.add_tool(WordListTool);
@@ -1788,6 +1799,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: "{}".into(),
input: json!({}),
is_input_complete: true,
thought_signature: None,
};
let echo_tool_use = LanguageModelToolUse {
id: "tool_id_2".into(),
@@ -1795,6 +1807,7 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model.send_last_completion_stream_text_chunk("Hi!");
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
@@ -1851,7 +1864,6 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
// Initialize language model system with test provider
cx.update(|cx| {
gpui_tokio::init(cx);
client::init_settings(cx);
let http_client = FakeHttpClient::with_404_response();
let clock = Arc::new(clock::FakeSystemClock::new());
@@ -1859,9 +1871,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
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);
Project::init_settings(cx);
LanguageModelRegistry::test(cx);
agent_settings::init(cx);
});
cx.executor().forbid_parking();
@@ -2003,6 +2013,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(),
input,
is_input_complete: false,
thought_signature: None,
},
));
@@ -2015,6 +2026,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
raw_input: input.to_string(),
input,
is_input_complete: true,
thought_signature: None,
},
));
fake_model.end_last_completion_stream();
@@ -2217,6 +2229,7 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
raw_input: json!({"text": "test"}).to_string(),
input: json!({"text": "test"}),
is_input_complete: true,
thought_signature: None,
};
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(
tool_use_1.clone(),
@@ -2395,8 +2408,6 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
cx.update(|cx| {
settings::init(cx);
Project::init_settings(cx);
agent_settings::init(cx);
match model {
TestModel::Fake => {}
@@ -2404,7 +2415,6 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
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);

View File

@@ -30,16 +30,17 @@ use gpui::{
};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt,
LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
LanguageModelId, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool,
LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage,
ZED_CLOUD_PROVIDER_ID,
};
use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use settings::{LanguageModelSelection, Settings, update_settings_file};
use smol::stream::StreamExt;
use std::{
collections::BTreeMap,
@@ -606,6 +607,8 @@ pub struct Thread {
pub(crate) prompt_capabilities_rx: watch::Receiver<acp::PromptCapabilities>,
pub(crate) project: Entity<Project>,
pub(crate) action_log: Entity<ActionLog>,
/// Tracks the last time files were read by the agent, to detect external modifications
pub(crate) file_read_times: HashMap<PathBuf, fs::MTime>,
}
impl Thread {
@@ -664,6 +667,7 @@ impl Thread {
prompt_capabilities_rx,
project,
action_log,
file_read_times: HashMap::default(),
}
}
@@ -798,7 +802,8 @@ impl Thread {
let profile_id = db_thread
.profile
.unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone());
let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
let mut model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
db_thread
.model
.and_then(|model| {
@@ -811,6 +816,16 @@ impl Thread {
.or_else(|| registry.default_model())
.map(|model| model.model)
});
if model.is_none() {
model = Self::resolve_profile_model(&profile_id, cx);
}
if model.is_none() {
model = LanguageModelRegistry::global(cx).update(cx, |registry, _cx| {
registry.default_model().map(|model| model.model)
});
}
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
@@ -848,6 +863,7 @@ impl Thread {
updated_at: db_thread.updated_at,
prompt_capabilities_tx,
prompt_capabilities_rx,
file_read_times: HashMap::default(),
}
}
@@ -987,6 +1003,7 @@ impl Thread {
self.add_tool(NowTool);
self.add_tool(OpenTool::new(self.project.clone()));
self.add_tool(ReadFileTool::new(
cx.weak_entity(),
self.project.clone(),
self.action_log.clone(),
));
@@ -1007,8 +1024,17 @@ impl Thread {
&self.profile_id
}
pub fn set_profile(&mut self, profile_id: AgentProfileId) {
pub fn set_profile(&mut self, profile_id: AgentProfileId, cx: &mut Context<Self>) {
if self.profile_id == profile_id {
return;
}
self.profile_id = profile_id;
// Swap to the profile's preferred model when available.
if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) {
self.set_model(model, cx);
}
}
pub fn cancel(&mut self, cx: &mut Context<Self>) {
@@ -1065,6 +1091,35 @@ impl Thread {
})
}
/// Look up the active profile and resolve its preferred model if one is configured.
fn resolve_profile_model(
profile_id: &AgentProfileId,
cx: &mut Context<Self>,
) -> Option<Arc<dyn LanguageModel>> {
let selection = AgentSettings::get_global(cx)
.profiles
.get(profile_id)?
.default_model
.clone()?;
Self::resolve_model_from_selection(&selection, cx)
}
/// Translate a stored model selection into the configured model from the registry.
fn resolve_model_from_selection(
selection: &LanguageModelSelection,
cx: &mut Context<Self>,
) -> Option<Arc<dyn LanguageModel>> {
let selected = SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
model: LanguageModelId::from(selection.model.clone()),
};
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry
.select_model(&selected, cx)
.map(|configured| configured.model)
})
}
pub fn resume(
&mut self,
cx: &mut Context<Self>,
@@ -2139,7 +2194,7 @@ where
/// Returns the JSON schema that describes the tool's input.
fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema {
crate::tool_schema::root_schema_for::<Self::Input>(format)
language_model::tool_schema::root_schema_for::<Self::Input>(format)
}
/// Some tools rely on a provider for the underlying billing or other reasons.
@@ -2226,7 +2281,7 @@ where
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
let mut json = serde_json::to_value(T::input_schema(format))?;
crate::tool_schema::adapt_schema_to_format(&mut json, format)?;
language_model::tool_schema::adapt_schema_to_format(&mut json, format)?;
Ok(json)
}

View File

@@ -165,7 +165,7 @@ impl AnyAgentTool for ContextServerTool {
format: language_model::LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let mut schema = self.tool.input_schema.clone();
crate::tool_schema::adapt_schema_to_format(&mut schema, format)?;
language_model::tool_schema::adapt_schema_to_format(&mut schema, format)?;
Ok(match schema {
serde_json::Value::Null => {
serde_json::json!({ "type": "object", "properties": [] })

View File

@@ -309,6 +309,40 @@ impl AgentTool for EditFileTool {
})?
.await?;
// Check if the file has been modified since the agent last read it
if let Some(abs_path) = abs_path.as_ref() {
let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| {
let last_read = thread.file_read_times.get(abs_path).copied();
let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime());
let dirty = buffer.read(cx).is_dirty();
(last_read, current, dirty)
})?;
// Check for unsaved changes first - these indicate modifications we don't know about
if is_dirty {
anyhow::bail!(
"This file cannot be written to because it has unsaved changes. \
Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \
Ask the user to save that buffer's changes and to inform you when it's ok to proceed."
);
}
// Check if the file was modified on disk since we last read it
if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) {
// MTime can be unreliable for comparisons, so our newtype intentionally
// doesn't support comparing them. If the mtime at all different
// (which could be because of a modification or because e.g. system clock changed),
// we pessimistically assume it was modified.
if current != last_read {
anyhow::bail!(
"The file {} has been modified since you last read it. \
Please read the file again to get the current state before editing it.",
input.path.display()
);
}
}
}
let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?;
event_stream.update_diff(diff.clone());
let _finalize_diff = util::defer({
@@ -421,6 +455,17 @@ impl AgentTool for EditFileTool {
log.buffer_edited(buffer.clone(), cx);
})?;
// Update the recorded read time after a successful edit so consecutive edits work
if let Some(abs_path) = abs_path.as_ref() {
if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| {
buffer.file().and_then(|file| file.disk_state().mtime())
})? {
self.thread.update(cx, |thread, _| {
thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime);
})?;
}
}
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let (new_text, unified_diff) = cx
.background_spawn({
@@ -562,7 +607,6 @@ fn resolve_path(
mod tests {
use super::*;
use crate::{ContextServerRegistry, Templates};
use client::TelemetrySettings;
use fs::Fs;
use gpui::{TestAppContext, UpdateGlobal};
use language_model::fake_provider::FakeLanguageModel;
@@ -1749,14 +1793,426 @@ mod tests {
}
}
#[gpui::test]
async fn test_file_read_times_tracking(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
// Initially, file_read_times should be empty
let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty());
assert!(is_empty, "file_read_times should start empty");
// Create read tool
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
// Read the file to record the read time
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Verify that file_read_times now contains an entry for the file
let has_entry = thread.read_with(cx, |thread, _| {
thread.file_read_times.len() == 1
&& thread
.file_read_times
.keys()
.any(|path| path.ends_with("test.txt"))
});
assert!(
has_entry,
"file_read_times should contain an entry after reading the file"
);
// Read the file again - should update the entry
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Should still have exactly one entry
let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1);
assert!(
has_one_entry,
"file_read_times should still have one entry after re-reading"
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
TelemetrySettings::register(cx);
agent_settings::AgentSettings::register(cx);
Project::init_settings(cx);
});
}
#[gpui::test]
async fn test_consecutive_edits_work(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// First edit should work
let edit_result = {
let edit_task = cx.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "First edit".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
});
cx.executor().run_until_parked();
model.send_last_completion_stream_text_chunk(
"<old_text>original content</old_text><new_text>modified content</new_text>"
.to_string(),
);
model.end_last_completion_stream();
edit_task.await
};
assert!(
edit_result.is_ok(),
"First edit should succeed, got error: {:?}",
edit_result.as_ref().err()
);
// Second edit should also work because the edit updated the recorded read time
let edit_result = {
let edit_task = cx.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Second edit".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
});
cx.executor().run_until_parked();
model.send_last_completion_stream_text_chunk(
"<old_text>modified content</old_text><new_text>further modified content</new_text>".to_string(),
);
model.end_last_completion_stream();
edit_task.await
};
assert!(
edit_result.is_ok(),
"Second consecutive edit should succeed, got error: {:?}",
edit_result.as_ref().err()
);
}
#[gpui::test]
async fn test_external_modification_detected(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Simulate external modification - advance time and save file
cx.background_executor
.advance_clock(std::time::Duration::from_secs(2));
fs.save(
path!("/root/test.txt").as_ref(),
&"externally modified content".into(),
language::LineEnding::Unix,
)
.await
.unwrap();
// Reload the buffer to pick up the new mtime
let project_path = project
.read_with(cx, |project, cx| {
project.find_project_path("root/test.txt", cx)
})
.expect("Should find project path");
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await
.unwrap();
buffer
.update(cx, |buffer, cx| buffer.reload(cx))
.await
.unwrap();
cx.executor().run_until_parked();
// Try to edit - should fail because file was modified externally
let result = cx
.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Edit after external change".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert!(
result.is_err(),
"Edit should fail after external modification"
);
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("has been modified since you last read it"),
"Error should mention file modification, got: {}",
error_msg
);
}
#[gpui::test]
async fn test_dirty_buffer_detected(cx: &mut TestAppContext) {
init_test(cx);
let fs = project::FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"test.txt": "original content"
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model.clone()),
cx,
)
});
let languages = project.read_with(cx, |project, _| project.languages().clone());
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
let read_tool = Arc::new(crate::ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log,
));
let edit_tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages,
Templates::new(),
));
// Read the file first
cx.update(|cx| {
read_tool.clone().run(
crate::ReadFileToolInput {
path: "root/test.txt".to_string(),
start_line: None,
end_line: None,
},
ToolCallEventStream::test().0,
cx,
)
})
.await
.unwrap();
// Open the buffer and make it dirty by editing without saving
let project_path = project
.read_with(cx, |project, cx| {
project.find_project_path("root/test.txt", cx)
})
.expect("Should find project path");
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.await
.unwrap();
// Make an in-memory edit to the buffer (making it dirty)
buffer.update(cx, |buffer, cx| {
let end_point = buffer.max_point();
buffer.edit([(end_point..end_point, " added text")], None, cx);
});
// Verify buffer is dirty
let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty());
assert!(is_dirty, "Buffer should be dirty after in-memory edit");
// Try to edit - should fail because buffer has unsaved changes
let result = cx
.update(|cx| {
edit_tool.clone().run(
EditFileToolInput {
display_description: "Edit with dirty buffer".into(),
path: "root/test.txt".into(),
mode: EditFileMode::Edit,
},
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert!(result.is_err(), "Edit should fail when buffer is dirty");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot be written to because it has unsaved changes"),
"Error should mention unsaved changes, got: {}",
error_msg
);
}
}

View File

@@ -246,8 +246,6 @@ mod test {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -778,8 +778,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}

View File

@@ -223,8 +223,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}

View File

@@ -163,8 +163,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -1,7 +1,7 @@
use action_log::ActionLog;
use agent_client_protocol::{self as acp, ToolCallUpdateFields};
use anyhow::{Context as _, Result, anyhow};
use gpui::{App, Entity, SharedString, Task};
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use indoc::formatdoc;
use language::Point;
use language_model::{LanguageModelImage, LanguageModelToolResultContent};
@@ -12,7 +12,7 @@ use settings::Settings;
use std::sync::Arc;
use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream, outline};
use crate::{AgentTool, Thread, ToolCallEventStream, outline};
/// Reads the content of the given file in the project.
///
@@ -42,13 +42,19 @@ pub struct ReadFileToolInput {
}
pub struct ReadFileTool {
thread: WeakEntity<Thread>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
}
impl ReadFileTool {
pub fn new(project: Entity<Project>, action_log: Entity<ActionLog>) -> Self {
pub fn new(
thread: WeakEntity<Thread>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
) -> Self {
Self {
thread,
project,
action_log,
}
@@ -195,6 +201,17 @@ impl AgentTool for ReadFileTool {
anyhow::bail!("{file_path} not found");
}
// Record the file read time and mtime
if let Some(mtime) = buffer.read_with(cx, |buffer, _| {
buffer.file().and_then(|file| file.disk_state().mtime())
})? {
self.thread
.update(cx, |thread, _| {
thread.file_read_times.insert(abs_path.to_path_buf(), mtime);
})
.ok();
}
let mut anchor = None;
// Check if specific line ranges are provided
@@ -285,11 +302,15 @@ impl AgentTool for ReadFileTool {
#[cfg(test)]
mod test {
use super::*;
use crate::{ContextServerRegistry, Templates, Thread};
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use prompt_store::ProjectContext;
use serde_json::json;
use settings::SettingsStore;
use std::sync::Arc;
use util::path;
#[gpui::test]
@@ -300,7 +321,20 @@ mod test {
fs.insert_tree(path!("/root"), json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let (event_stream, _) = ToolCallEventStream::test();
let result = cx
@@ -333,7 +367,20 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let result = cx
.update(|cx| {
let input = ReadFileToolInput {
@@ -363,7 +410,20 @@ mod test {
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let result = cx
.update(|cx| {
let input = ReadFileToolInput {
@@ -435,7 +495,20 @@ mod test {
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
let result = cx
.update(|cx| {
let input = ReadFileToolInput {
@@ -463,7 +536,20 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
// start_line of 0 should be treated as 1
let result = cx
@@ -509,8 +595,6 @@ mod test {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
@@ -609,7 +693,20 @@ mod test {
let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project, action_log));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log));
// Reading a file outside the project worktree should fail
let result = cx
@@ -823,7 +920,24 @@ mod test {
.await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let tool = Arc::new(ReadFileTool::new(project.clone(), action_log.clone()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
Some(model),
cx,
)
});
let tool = Arc::new(ReadFileTool::new(
thread.downgrade(),
project.clone(),
action_log.clone(),
));
// Test reading allowed files in worktree1
let result = cx

View File

@@ -21,7 +21,6 @@ acp_tools.workspace = true
acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
async-trait.workspace = true
client.workspace = true
@@ -33,7 +32,6 @@ gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
http_client.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true

View File

@@ -29,6 +29,7 @@ pub struct UnsupportedVersion;
pub struct AcpConnection {
server_name: SharedString,
telemetry_id: &'static str,
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
@@ -52,6 +53,7 @@ pub struct AcpSession {
pub async fn connect(
server_name: SharedString,
telemetry_id: &'static str,
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
@@ -60,6 +62,7 @@ pub async fn connect(
) -> Result<Rc<dyn AgentConnection>> {
let conn = AcpConnection::stdio(
server_name,
telemetry_id,
command.clone(),
root_dir,
default_mode,
@@ -75,6 +78,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
server_name: SharedString,
telemetry_id: &'static str,
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
@@ -132,7 +136,7 @@ impl AcpConnection {
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
log::warn!("agent stderr: {}", &line);
log::warn!("agent stderr: {}", line.trim());
line.clear();
}
Ok(())
@@ -199,6 +203,7 @@ impl AcpConnection {
root_dir: root_dir.to_owned(),
connection,
server_name,
telemetry_id,
sessions,
agent_capabilities: response.agent_capabilities,
default_mode,
@@ -226,6 +231,10 @@ impl Drop for AcpConnection {
}
impl AgentConnection for AcpConnection {
fn telemetry_id(&self) -> &'static str {
self.telemetry_id
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
@@ -238,37 +247,58 @@ impl AgentConnection for AcpConnection {
let default_mode = self.default_mode.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
let mcp_servers =
if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
match &*configuration {
project::context_server_store::ContextServerConfiguration::Custom {
command,
..
}
| project::context_server_store::ContextServerConfiguration::Extension {
command,
..
} => Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
}),
project::context_server_store::ContextServerConfiguration::Http {
url,
headers,
} => Some(acp::McpServer::Http {
name: id.0.to_string(),
url: url.to_string(),
headers: headers.iter().map(|(name, value)| acp::HttpHeader {
name: name.clone(),
value: value.clone(),
meta: None,
}).collect(),
}),
}
})
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
cx.spawn(async move |cx| {
let response = conn

View File

@@ -62,6 +62,7 @@ impl AgentServer for ClaudeCode {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let telemetry_id = self.telemetry_id();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
@@ -85,6 +86,7 @@ impl AgentServer for ClaudeCode {
.await?;
let connection = crate::acp::connect(
name,
telemetry_id,
command,
root_dir.as_ref(),
default_mode,

View File

@@ -63,6 +63,7 @@ impl AgentServer for Codex {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let telemetry_id = self.telemetry_id();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
@@ -87,6 +88,7 @@ impl AgentServer for Codex {
let connection = crate::acp::connect(
name,
telemetry_id,
command,
root_dir.as_ref(),
default_mode,

View File

@@ -50,13 +50,14 @@ impl crate::AgentServer for CustomAgentServer {
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
settings
if let Some(settings) = settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
.unwrap()
.default_mode = mode_id.map(|m| m.to_string())
{
settings.default_mode = mode_id.map(|m| m.to_string())
}
});
}
@@ -67,6 +68,7 @@ impl crate::AgentServer for CustomAgentServer {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let telemetry_id = self.telemetry_id();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
@@ -92,6 +94,7 @@ impl crate::AgentServer for CustomAgentServer {
.await?;
let connection = crate::acp::connect(
name,
telemetry_id,
command,
root_dir.as_ref(),
default_mode,

View File

@@ -6,7 +6,9 @@ use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
#[cfg(test)]
use project::agent_server_store::BuiltinAgentServerSettings;
use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
use project::{FakeFs, Project};
#[cfg(test)]
use settings::Settings;
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -452,29 +454,22 @@ pub use common_e2e_tests;
// Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
use settings::Settings;
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = settings::SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
language::init(cx);
gpui_tokio::init(cx);
let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
client::init_settings(cx);
let client = client::Client::production(cx);
let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client, cx);
agent_settings::init(cx);
AllAgentServersSettings::register(cx);
#[cfg(test)]
AllAgentServersSettings::override_global(
AllAgentServersSettings {
project::agent_server_store::AllAgentServersSettings::override_global(
project::agent_server_store::AllAgentServersSettings {
claude: Some(BuiltinAgentServerSettings {
path: Some("claude-code-acp".into()),
args: None,

View File

@@ -31,6 +31,7 @@ impl AgentServer for Gemini {
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let telemetry_id = self.telemetry_id();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
@@ -64,6 +65,7 @@ impl AgentServer for Gemini {
let connection = crate::acp::connect(
name,
telemetry_id,
command,
root_dir.as_ref(),
default_mode,

View File

@@ -6,8 +6,8 @@ use convert_case::{Case, Casing as _};
use fs::Fs;
use gpui::{App, SharedString};
use settings::{
AgentProfileContent, ContextServerPresetContent, Settings as _, SettingsContent,
update_settings_file,
AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _,
SettingsContent, update_settings_file,
};
use util::ResultExt as _;
@@ -53,19 +53,30 @@ impl AgentProfile {
let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
// Copy toggles from the base profile so the new profile starts with familiar defaults.
let tools = base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default();
let enable_all_context_servers = base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default();
let context_servers = base_profile
.as_ref()
.map(|profile| profile.context_servers.clone())
.unwrap_or_default();
// Preserve the base profile's model preference when cloning into a new profile.
let default_model = base_profile
.as_ref()
.and_then(|profile| profile.default_model.clone());
let profile_settings = AgentProfileSettings {
name: name.into(),
tools: base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default(),
enable_all_context_servers: base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default(),
context_servers: base_profile
.map(|profile| profile.context_servers)
.unwrap_or_default(),
tools,
enable_all_context_servers,
context_servers,
default_model,
};
update_settings_file(fs, cx, {
@@ -96,6 +107,8 @@ pub struct AgentProfileSettings {
pub tools: IndexMap<Arc<str>, bool>,
pub enable_all_context_servers: bool,
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
/// Default language model to apply when this profile becomes active.
pub default_model: Option<LanguageModelSelection>,
}
impl AgentProfileSettings {
@@ -144,6 +157,7 @@ impl AgentProfileSettings {
)
})
.collect(),
default_model: self.default_model.clone(),
},
);
@@ -153,15 +167,23 @@ impl AgentProfileSettings {
impl From<AgentProfileContent> for AgentProfileSettings {
fn from(content: AgentProfileContent) -> Self {
let AgentProfileContent {
name,
tools,
enable_all_context_servers,
context_servers,
default_model,
} = content;
Self {
name: content.name.into(),
tools: content.tools,
enable_all_context_servers: content.enable_all_context_servers.unwrap_or_default(),
context_servers: content
.context_servers
name: name.into(),
tools,
enable_all_context_servers: enable_all_context_servers.unwrap_or_default(),
context_servers: context_servers
.into_iter()
.map(|(server_id, preset)| (server_id, preset.into()))
.collect(),
default_model,
}
}
}

View File

@@ -10,7 +10,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
NotifyWhenAgentWaiting, Settings,
NotifyWhenAgentWaiting, RegisterSetting, Settings,
};
pub use crate::agent_profile::*;
@@ -19,11 +19,7 @@ pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread
pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str =
include_str!("prompts/summarize_thread_detailed_prompt.txt");
pub fn init(cx: &mut App) {
AgentSettings::register(cx);
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, RegisterSetting)]
pub struct AgentSettings {
pub enabled: bool,
pub button: bool,

View File

@@ -14,6 +14,7 @@ doctest = false
[features]
test-support = ["gpui/test-support", "language/test-support"]
unit-eval = []
[dependencies]
acp_thread.workspace = true
@@ -47,6 +48,7 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
@@ -98,12 +100,15 @@ util.workspace = true
watch.workspace = true
workspace.workspace = true
zed_actions.workspace = true
image.workspace = true
async-fs.workspace = true
[dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
assistant_text_thread = { workspace = true, features = ["test-support"] }
buffer_diff = { workspace = true, features = ["test-support"] }
clock.workspace = true
db = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }

View File

@@ -4,7 +4,6 @@ mod message_editor;
mod mode_selector;
mod model_selector;
mod model_selector_popover;
mod thread_editor;
mod thread_history;
mod thread_view;

View File

@@ -109,6 +109,8 @@ impl ContextPickerCompletionProvider {
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -146,6 +148,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
icon_path: Some(icon_for_completion),
confirm: Some(confirm_completion_callback(
thread_entry.title().clone(),
@@ -177,6 +181,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
icon_path: Some(icon_path),
confirm: Some(confirm_completion_callback(
rule.title,
@@ -233,6 +239,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
file_name,
@@ -284,6 +292,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
symbol.name.into(),
@@ -316,6 +326,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
url_to_fetch.to_string().into(),
@@ -384,6 +396,8 @@ impl ContextPickerCompletionProvider {
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -646,16 +660,14 @@ impl ContextPickerCompletionProvider {
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let embedded_context = self.prompt_capabilities.borrow().embedded_context;
let mut entries = if embedded_context {
vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
ContextPickerEntry::Mode(ContextPickerMode::Thread),
]
} else {
// File is always available, but we don't need a mode entry
vec![]
};
let mut entries = vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
];
if embedded_context {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
}
let has_selection = workspace
.read(cx)
@@ -696,14 +708,18 @@ fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -
}
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let path = cx
.theme()
.syntax()
.highlight_id("variable")
.map(HighlightId);
let mut label = CodeLabelBuilder::default();
label.push_str(file_name, None);
label.push_str(" ", None);
if let Some(directory) = directory {
label.push_str(directory, comment_id);
label.push_str(directory, path);
}
label.build()
@@ -772,6 +788,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
)),
source: project::CompletionSource::Custom,
icon_path: None,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(Arc::new({
let editor = editor.clone();

View File

@@ -401,10 +401,9 @@ mod tests {
use acp_thread::{AgentConnection, StubAgentConnection};
use agent::HistoryStore;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use assistant_text_thread::TextThreadStore;
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::{EditorSettings, RowInfo};
use editor::RowInfo;
use fs::FakeFs;
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
@@ -413,7 +412,7 @@ mod tests {
use pretty_assertions::assert_matches;
use project::Project;
use serde_json::json;
use settings::{Settings as _, SettingsStore};
use settings::SettingsStore;
use util::path;
use workspace::Workspace;
@@ -539,13 +538,8 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});
}
}

View File

@@ -15,6 +15,7 @@ use editor::{
EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
MultiBuffer, ToOffset,
actions::Paste,
code_context_menus::CodeContextMenu,
display_map::{Crease, CreaseId, FoldId},
scroll::Autoscroll,
};
@@ -27,6 +28,7 @@ use gpui::{
EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
Subscription, Task, TextStyle, WeakEntity, pulsating_between,
};
use itertools::Either;
use language::{Buffer, Language, language_settings::InlayHintKind};
use language_model::LanguageModelImage;
use postage::stream::Stream as _;
@@ -272,6 +274,15 @@ impl MessageEditor {
self.editor.read(cx).is_empty(cx)
}
pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
self.editor
.read(cx)
.context_menu()
.borrow()
.as_ref()
.is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
}
pub fn mentions(&self) -> HashSet<MentionUri> {
self.mention_set
.mentions
@@ -356,7 +367,7 @@ impl MessageEditor {
let task = match mention_uri.clone() {
MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
MentionUri::Directory { .. } => Task::ready(Ok(Mention::UriOnly)),
MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
@@ -373,7 +384,6 @@ impl MessageEditor {
)))
}
MentionUri::Selection { .. } => {
// Handled elsewhere
debug_panic!("unexpected selection URI");
Task::ready(Err(anyhow!("unexpected selection URI")))
}
@@ -704,20 +714,21 @@ impl MessageEditor {
return Task::ready(Err(err));
}
let contents = self.mention_set.contents(
&self.prompt_capabilities.borrow(),
full_mention_content,
self.project.clone(),
cx,
);
let contents = self
.mention_set
.contents(full_mention_content, self.project.clone(), cx);
let editor = self.editor.clone();
let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
cx.spawn(async move |_, cx| {
let contents = contents.await?;
let mut all_tracked_buffers = Vec::new();
let result = editor.update(cx, |editor, cx| {
let mut ix = text.chars().position(|c| !c.is_whitespace()).unwrap_or(0);
let (mut ix, _) = text
.char_indices()
.find(|(_, c)| !c.is_whitespace())
.unwrap_or((0, '\0'));
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let text = editor.text(cx);
editor.display_map.update(cx, |map, cx| {
@@ -738,18 +749,32 @@ impl MessageEditor {
tracked_buffers,
} => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
meta: None,
})
if supports_embedded_context {
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource:
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
meta: None,
})
} else {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
}
}
Mention::Image(mention_image) => {
let uri = match uri {
@@ -771,18 +796,16 @@ impl MessageEditor {
meta: None,
})
}
Mention::UriOnly => {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
}
Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
};
chunks.push(chunk);
ix = crease_range.end;
@@ -824,6 +847,45 @@ impl MessageEditor {
cx.emit(MessageEditorEvent::Send)
}
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
cx.spawn_in(window, async move |_, cx| {
editor
.update_in(cx, |editor, window, cx| {
let menu_is_open =
editor.context_menu().borrow().as_ref().is_some_and(|menu| {
matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
});
let has_at_sign = {
let snapshot = editor.display_snapshot(cx);
let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
let offset = cursor.to_offset(&snapshot);
if offset > 0 {
snapshot
.buffer_snapshot()
.reversed_chars_at(offset)
.next()
.map(|sign| sign == '@')
.unwrap_or(false)
} else {
false
}
};
if menu_is_open && has_at_sign {
return;
}
editor.insert("@", window, cx);
editor.show_completions(&editor::actions::ShowCompletions, window, cx);
})
.log_err();
})
.detach();
}
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
self.send(cx);
}
@@ -851,74 +913,114 @@ impl MessageEditor {
if !self.prompt_capabilities.borrow().image {
return;
}
let images = cx
.read_from_clipboard()
.map(|item| {
item.into_entries()
.filter_map(|entry| {
if let ClipboardEntry::Image(image) = entry {
Some(image)
} else {
None
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if images.is_empty() {
let Some(clipboard) = cx.read_from_clipboard() else {
return;
}
cx.stop_propagation();
};
cx.spawn_in(window, async move |this, cx| {
use itertools::Itertools;
let (mut images, paths) = clipboard
.into_entries()
.filter_map(|entry| match entry {
ClipboardEntry::Image(image) => Some(Either::Left(image)),
ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
_ => None,
})
.partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
let replacement_text = MentionUri::PastedImage.as_link().to_string();
for image in images {
let (excerpt_id, text_anchor, multibuffer_anchor) =
self.editor.update(cx, |message_editor, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot().as_singleton().unwrap();
if !paths.is_empty() {
images.extend(
cx.background_spawn(async move {
let mut images = vec![];
for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
let Ok(content) = async_fs::read(path).await else {
continue;
};
let Ok(format) = image::guess_format(&content) else {
continue;
};
images.push(gpui::Image::from_bytes(
match format {
image::ImageFormat::Png => gpui::ImageFormat::Png,
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
_ => continue,
},
content,
));
}
images
})
.await,
);
}
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit(
[(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
format!("{replacement_text} "),
)],
if images.is_empty() {
return;
}
let replacement_text = MentionUri::PastedImage.as_link().to_string();
let Ok(editor) = this.update(cx, |this, cx| {
cx.stop_propagation();
this.editor.clone()
}) else {
return;
};
for image in images {
let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
editor.update_in(cx, |message_editor, window, cx| {
let snapshot = message_editor.snapshot(window, cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.buffer_snapshot().as_singleton().unwrap();
let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
let multibuffer_anchor = snapshot
.buffer_snapshot()
.anchor_in_excerpt(*excerpt_id, text_anchor);
message_editor.edit(
[(
multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
format!("{replacement_text} "),
)],
cx,
);
(*excerpt_id, text_anchor, multibuffer_anchor)
})
else {
break;
};
let content_len = replacement_text.len();
let Some(start_anchor) = multibuffer_anchor else {
continue;
};
let Ok(end_anchor) = editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
}) else {
continue;
};
let image = Arc::new(image);
let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
MentionUri::PastedImage.name().into(),
IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
editor.clone(),
window,
cx,
);
(*excerpt_id, text_anchor, multibuffer_anchor)
});
let content_len = replacement_text.len();
let Some(start_anchor) = multibuffer_anchor else {
continue;
};
let end_anchor = self.editor.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
});
let image = Arc::new(image);
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
MentionUri::PastedImage.name().into(),
IconName::Image.path().into(),
Some(Task::ready(Ok(image.clone())).shared()),
self.editor.clone(),
window,
cx,
) else {
continue;
};
let task = cx
.spawn_in(window, {
async move |_, cx| {
)
}) else {
continue;
};
let task = cx
.spawn(async move |cx| {
let format = image.format;
let image = cx
.update(|_, cx| LanguageModelImage::from_image(image, cx))
@@ -933,15 +1035,16 @@ impl MessageEditor {
} else {
Err("Failed to convert image".into())
}
}
})
.shared();
this.update(cx, |this, _| {
this.mention_set
.mentions
.insert(crease_id, (MentionUri::PastedImage, task.clone()))
})
.shared();
.ok();
self.mention_set
.mentions
.insert(crease_id, (MentionUri::PastedImage, task.clone()));
cx.spawn_in(window, async move |this, cx| {
if task.await.notify_async_err(cx).is_none() {
this.update(cx, |this, cx| {
this.editor.update(cx, |editor, cx| {
@@ -951,9 +1054,9 @@ impl MessageEditor {
})
.ok();
}
})
.detach();
}
}
})
.detach();
}
pub fn insert_dragged_files(
@@ -1111,7 +1214,7 @@ impl MessageEditor {
let start = text.len();
write!(&mut text, "{}", mention_uri.as_link()).ok();
let end = text.len();
mentions.push((start..end, mention_uri, Mention::UriOnly));
mentions.push((start..end, mention_uri, Mention::Link));
}
}
acp::ContentBlock::Image(acp::ImageContent {
@@ -1183,6 +1286,17 @@ impl MessageEditor {
self.editor.read(cx).text(cx)
}
pub fn set_placeholder_text(
&mut self,
placeholder: &str,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.set_placeholder_text(placeholder, window, cx);
});
}
#[cfg(test)]
pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
@@ -1517,7 +1631,7 @@ pub enum Mention {
tracked_buffers: Vec<Entity<Buffer>>,
},
Image(MentionImage),
UriOnly,
Link,
}
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -1534,21 +1648,10 @@ pub struct MentionSet {
impl MentionSet {
fn contents(
&self,
prompt_capabilities: &acp::PromptCapabilities,
full_mention_content: bool,
project: Entity<Project>,
cx: &mut App,
) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
if !prompt_capabilities.embedded_context {
let mentions = self
.mentions
.iter()
.map(|(crease_id, (uri, _))| (*crease_id, (uri.clone(), Mention::UriOnly)))
.collect();
return Task::ready(Ok(mentions));
}
let mentions = self.mentions.clone();
cx.spawn(async move |cx| {
let mut contents = HashMap::default();
@@ -1898,10 +2001,8 @@ mod tests {
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;
@@ -2074,10 +2175,8 @@ mod tests {
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);
});
app_state
@@ -2202,6 +2301,8 @@ mod tests {
format!("seven.txt b{slash}"),
format!("six.txt b{slash}"),
format!("five.txt b{slash}"),
"Files & Directories".into(),
"Symbols".into()
]
);
editor.set_text("", window, cx);
@@ -2286,21 +2387,11 @@ mod tests {
assert_eq!(fold_ranges(editor, cx).len(), 1);
});
let all_prompt_capabilities = acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
};
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2318,30 +2409,6 @@ mod tests {
);
}
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&acp::PromptCapabilities::default(),
false,
project.clone(),
cx,
)
})
.await
.unwrap()
.into_values()
.collect::<Vec<_>>();
{
let [(uri, Mention::UriOnly)] = contents.as_slice() else {
panic!("Unexpected mentions");
};
pretty_assertions::assert_eq!(
uri,
&MentionUri::parse(&url_one, PathStyle::local()).unwrap()
);
}
cx.simulate_input(" ");
editor.update(&mut cx, |editor, cx| {
@@ -2377,12 +2444,9 @@ mod tests {
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2503,12 +2567,9 @@ mod tests {
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap()
@@ -2554,12 +2615,9 @@ mod tests {
// Getting the message contents fails
message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.expect_err("Should fail to load x.png");
@@ -2610,12 +2668,9 @@ mod tests {
// Now getting the contents succeeds, because the invalid mention was removed
let contents = message_editor
.update(&mut cx, |message_editor, cx| {
message_editor.mention_set().contents(
&all_prompt_capabilities,
false,
project.clone(),
cx,
)
message_editor
.mention_set()
.contents(false, project.clone(), cx)
})
.await
.unwrap();
@@ -2658,13 +2713,14 @@ mod tests {
}
#[gpui::test]
async fn test_large_file_mention_uses_outline(cx: &mut TestAppContext) {
async fn test_large_file_mention_fallback(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
// Create a large file that exceeds AUTO_OUTLINE_SIZE
const LINE: &str = "fn example_function() { /* some code */ }\n";
// Using plain text without a configured language, so no outline is available
const LINE: &str = "This is a line of text in the file\n";
let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE);
@@ -2675,8 +2731,8 @@ mod tests {
fs.insert_tree(
"/project",
json!({
"large_file.rs": large_content.clone(),
"small_file.rs": small_content,
"large_file.txt": large_content.clone(),
"small_file.txt": small_content,
}),
)
.await;
@@ -2722,7 +2778,7 @@ mod tests {
let large_file_abs_path = project.read_with(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap();
let worktree_root = worktree.read(cx).abs_path();
worktree_root.join("large_file.rs")
worktree_root.join("large_file.txt")
});
let large_file_task = message_editor.update(cx, |editor, cx| {
editor.confirm_mention_for_file(large_file_abs_path, cx)
@@ -2731,11 +2787,20 @@ mod tests {
let large_file_mention = large_file_task.await.unwrap();
match large_file_mention {
Mention::Text { content, .. } => {
// Should contain outline header for large files
assert!(content.contains("File outline for"));
assert!(content.contains("file too large to show full content"));
// Should not contain the full repeated content
assert!(!content.contains(&LINE.repeat(100)));
// Should contain some of the content but not all of it
assert!(
content.contains(LINE),
"Should contain some of the file content"
);
assert!(
!content.contains(&LINE.repeat(100)),
"Should not contain the full file"
);
// Should be much smaller than original
assert!(
content.len() < large_content.len() / 10,
"Should be significantly truncated"
);
}
_ => panic!("Expected Text mention for large file"),
}
@@ -2745,7 +2810,7 @@ mod tests {
let small_file_abs_path = project.read_with(cx, |project, cx| {
let worktree = project.worktrees(cx).next().unwrap();
let worktree_root = worktree.read(cx).abs_path();
worktree_root.join("small_file.rs")
worktree_root.join("small_file.txt")
});
let small_file_task = message_editor.update(cx, |editor, cx| {
editor.confirm_mention_for_file(small_file_abs_path, cx)
@@ -2754,10 +2819,8 @@ mod tests {
let small_file_mention = small_file_task.await.unwrap();
match small_file_mention {
Mention::Text { content, .. } => {
// Should contain the actual content
// Should contain the full actual content
assert_eq!(content, small_content);
// Should not contain outline header
assert!(!content.contains("File outline for"));
}
_ => panic!("Expected Text mention for small file"),
}
@@ -2879,7 +2942,7 @@ mod tests {
cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| {
editor.set_text(" hello world ", window, cx);
editor.set_text(" \u{A0}してhello world ", window, cx);
});
let (content, _) = message_editor
@@ -2890,13 +2953,154 @@ mod tests {
assert_eq!(
content,
vec![acp::ContentBlock::Text(acp::TextContent {
text: "hello world".into(),
text: "してhello world".into(),
annotations: None,
meta: None
})]
);
}
#[gpui::test]
async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let file_content = "fn main() { println!(\"Hello, world!\"); }\n";
fs.insert_tree(
"/project",
json!({
"src": {
"main.rs": file_content,
}
}),
)
.await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
let (message_editor, editor) = workspace.update_in(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,
Default::default(),
Default::default(),
"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);
let editor = message_editor.read(cx).editor().clone();
(message_editor, editor)
});
cx.simulate_input("What is in @file main");
editor.update_in(cx, |editor, window, cx| {
assert!(editor.has_visible_completions_menu());
assert_eq!(editor.text(cx), "What is in @file main");
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
});
let content = message_editor
.update(cx, |editor, cx| editor.contents(false, cx))
.await
.unwrap()
.0;
let main_rs_uri = if cfg!(windows) {
"file:///C:/project/src/main.rs".to_string()
} else {
"file:///project/src/main.rs".to_string()
};
// When embedded context is `false` we should get a resource link
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: main_rs_uri.clone(),
name: "main.rs".to_string(),
annotations: None,
meta: None,
description: None,
mime_type: None,
size: None,
title: None,
})
]
);
message_editor.update(cx, |editor, _cx| {
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
..Default::default()
})
});
let content = message_editor
.update(cx, |editor, cx| editor.contents(false, cx))
.await
.unwrap()
.0;
// When embedded context is `true` we should get a resource
pretty_assertions::assert_eq!(
content,
vec![
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
text: file_content.to_string(),
uri: main_rs_uri,
mime_type: None,
meta: None
}
),
annotations: None,
meta: None
})
]
);
}
#[gpui::test]
async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) {
init_test(cx);
@@ -2904,10 +3108,8 @@ mod tests {
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);
});
app_state

View File

@@ -56,6 +56,10 @@ impl ModeSelector {
self.set_mode(all_modes[next_index].id.clone(), cx);
}
pub fn mode(&self) -> acp::SessionModeId {
self.connection.current_mode()
}
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
let task = self.connection.set_mode(mode, cx);
self.setting_mode = true;

View File

@@ -251,17 +251,17 @@ impl PickerDelegate for AcpModelPickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.start_slot::<Icon>(model_info.icon.map(|icon| {
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
}))
.child(
h_flex()
.w_full()
.pl_0p5()
.gap_1p5()
.w(px(240.))
.when_some(model_info.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small)
)
})
.child(Label::new(model_info.name.clone()).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {

View File

@@ -1,6 +1,6 @@
use std::rc::Rc;
use acp_thread::AgentModelSelector;
use acp_thread::{AgentModelInfo, AgentModelSelector};
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{
@@ -36,12 +36,8 @@ impl AcpModelSelectorPopover {
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())
pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
self.selector.read(cx).delegate.active_model()
}
}

View File

@@ -1,6 +0,0 @@
use acp_thread::AcpThread;
use gpui::Entity;
pub struct ThreadEditor {
thread: Entity<AcpThread>,
}

View File

@@ -457,25 +457,23 @@ impl Render for AcpThreadHistory {
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread))
.when(!self.history_store.read(cx).is_empty(cx), |parent| {
parent.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
.w_full()
.py_1()
.px_2()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(self.search_editor.clone()),
)
})
.child(
h_flex()
.h(px(41.)) // Match the toolbar perfectly
.w_full()
.py_1()
.px_2()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.color(Color::Muted)
.size(IconSize::Small),
)
.child(self.search_editor.clone()),
)
.child({
let view = v_flex()
.id("list-container")
@@ -484,19 +482,15 @@ impl Render for AcpThreadHistory {
.flex_grow();
if self.history_store.read(cx).is_empty(cx) {
view.justify_center()
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else if self.search_produced_no_matches() {
view.justify_center().child(
h_flex().w_full().justify_center().child(
Label::new("No threads match your search.").size(LabelSize::Small),
),
view.justify_center().items_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else if self.search_produced_no_matches() {
view.justify_center()
.items_center()
.child(Label::new("No threads match your search.").size(LabelSize::Small))
} else {
view.child(
uniform_list(
@@ -673,7 +667,7 @@ impl EntryTimeFormat {
timezone,
time_format::TimestampFormat::EnhancedAbsolute,
),
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp),
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
}
}
}

View File

@@ -4,12 +4,12 @@ use acp_thread::{
ToolCallStatus, UserMessageId,
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use action_log::{ActionLog, ActionLogTelemetry};
use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use anyhow::{Result, anyhow, bail};
use anyhow::{Result, anyhow};
use arrayvec::ArrayVec;
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
@@ -51,7 +51,7 @@ use ui::{
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use workspace::{CollaboratorId, NewTerminal, Workspace};
use zed_actions::agent::{Chat, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary;
@@ -69,8 +69,8 @@ use crate::ui::{
};
use crate::{
AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode,
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll,
RejectOnce, ToggleBurnMode, ToggleProfileSelector,
CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory,
RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector,
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -125,8 +125,9 @@ impl ProfileProvider for Entity<agent::Thread> {
}
fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
self.update(cx, |thread, _cx| {
thread.set_profile(profile_id);
self.update(cx, |thread, cx| {
// Apply the profile and let the thread swap to its default model.
thread.set_profile(profile_id, cx);
});
}
@@ -169,7 +170,7 @@ impl ThreadFeedbackState {
}
}
let session_id = thread.read(cx).session_id().clone();
let agent_name = telemetry.agent_name();
let agent = thread.read(cx).connection().telemetry_id();
let task = telemetry.thread_data(&session_id, cx);
let rating = match feedback {
ThreadFeedback::Positive => "positive",
@@ -179,9 +180,9 @@ impl ThreadFeedbackState {
let thread = task.await?;
telemetry::event!(
"Agent Thread Rated",
agent = agent,
session_id = session_id,
rating = rating,
agent = agent_name,
thread = thread
);
anyhow::Ok(())
@@ -206,15 +207,15 @@ impl ThreadFeedbackState {
self.comments_editor.take();
let session_id = thread.read(cx).session_id().clone();
let agent_name = telemetry.agent_name();
let agent = thread.read(cx).connection().telemetry_id();
let task = telemetry.thread_data(&session_id, cx);
cx.background_spawn(async move {
let thread = task.await?;
telemetry::event!(
"Agent Thread Feedback Comments",
agent = agent,
session_id = session_id,
comments = comments,
agent = agent_name,
thread = thread
);
anyhow::Ok(())
@@ -277,6 +278,7 @@ pub struct AcpThreadView {
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
thread_retry_status: Option<RetryStatus>,
thread_error: Option<ThreadError>,
thread_error_markdown: Option<Entity<Markdown>>,
thread_feedback: ThreadFeedbackState,
list_state: ListState,
auth_task: Option<Task<()>>,
@@ -294,7 +296,6 @@ pub struct AcpThreadView {
resume_thread_metadata: Option<DbThreadMetadata>,
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5],
#[cfg(target_os = "windows")]
show_codex_windows_warning: bool,
}
@@ -337,19 +338,7 @@ impl AcpThreadView {
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
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"
|| agent.name() == "Codex"
|| !available_commands.borrow().is_empty()
{
format!(
"Message {} — @ to include context, / for commands",
agent.name()
)
} else {
format!("Message {} — @ to include context", agent.name())
};
let placeholder = placeholder_text(agent.name().as_ref(), false);
let message_editor = cx.new(|cx| {
let mut editor = MessageEditor::new(
@@ -401,7 +390,6 @@ impl AcpThreadView {
),
];
#[cfg(target_os = "windows")]
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
== Some(crate::ExternalAgent::Codex);
@@ -428,6 +416,7 @@ impl AcpThreadView {
list_state: list_state,
thread_retry_status: None,
thread_error: None,
thread_error_markdown: None,
thread_feedback: Default::default(),
auth_task: None,
expanded_tool_calls: HashSet::default(),
@@ -447,7 +436,6 @@ impl AcpThreadView {
focus_handle: cx.focus_handle(),
new_server_version_available: None,
resume_thread_metadata: resume_thread,
#[cfg(target_os = "windows")]
show_codex_windows_warning,
}
}
@@ -541,14 +529,7 @@ impl AcpThreadView {
})
.log_err()
} else {
let root_dir = if let Some(acp_agent) = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
{
acp_agent.root_dir().into()
} else {
root_dir.unwrap_or(paths::home_dir().as_path().into())
};
let root_dir = root_dir.unwrap_or(paths::home_dir().as_path().into());
cx.update(|_, cx| {
connection
.clone()
@@ -819,6 +800,7 @@ impl AcpThreadView {
if should_retry {
self.thread_error = None;
self.thread_error_markdown = None;
self.reset(window, cx);
}
}
@@ -1133,8 +1115,6 @@ impl AcpThreadView {
message_editor.contents(full_mention_content, cx)
});
let agent_telemetry_id = self.agent.telemetry_id();
self.thread_error.take();
self.editing_message.take();
self.thread_feedback.clear();
@@ -1142,6 +1122,8 @@ impl AcpThreadView {
let Some(thread) = self.thread() else {
return;
};
let agent_telemetry_id = self.agent.telemetry_id();
let session_id = thread.read(cx).session_id().clone();
let thread = thread.downgrade();
if self.should_be_following {
self.workspace
@@ -1152,6 +1134,8 @@ impl AcpThreadView {
}
self.is_loading_contents = true;
let model_id = self.current_model_id(cx);
let mode_id = self.current_mode_id(cx);
let guard = cx.new(|_| ());
cx.observe_release(&guard, |this, _guard, cx| {
this.is_loading_contents = false;
@@ -1173,6 +1157,7 @@ impl AcpThreadView {
message_editor.clear(window, cx);
});
})?;
let turn_start_time = Instant::now();
let send = thread.update(cx, |thread, cx| {
thread.action_log().update(cx, |action_log, cx| {
for buffer in tracked_buffers {
@@ -1181,11 +1166,29 @@ impl AcpThreadView {
});
drop(guard);
telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
telemetry::event!(
"Agent Message Sent",
agent = agent_telemetry_id,
session = session_id,
model = model_id,
mode = mode_id
);
thread.send(contents, cx)
})?;
send.await
let res = send.await;
let turn_time_ms = turn_start_time.elapsed().as_millis();
let status = if res.is_ok() { "success" } else { "failure" };
telemetry::event!(
"Agent Turn Completed",
agent = agent_telemetry_id,
session = session_id,
model = model_id,
mode = mode_id,
status,
turn_time_ms,
);
res
});
cx.spawn(async move |this, cx| {
@@ -1330,6 +1333,7 @@ impl AcpThreadView {
fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
self.thread_error = None;
self.thread_error_markdown = None;
cx.notify();
}
@@ -1387,7 +1391,7 @@ impl AcpThreadView {
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 model_or_agent_name = self.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);
@@ -1447,7 +1451,14 @@ impl AcpThreadView {
});
}
let has_commands = !available_commands.is_empty();
self.available_commands.replace(available_commands);
let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands);
self.message_editor.update(cx, |editor, cx| {
editor.set_placeholder_text(&new_placeholder, window, cx);
});
}
AcpThreadEvent::ModeUpdated(_mode) => {
// The connection keeps track of the mode
@@ -1856,6 +1867,14 @@ impl AcpThreadView {
let Some(thread) = self.thread() else {
return;
};
telemetry::event!(
"Agent Tool Call Authorized",
agent = self.agent.telemetry_id(),
session = thread.read(cx).session_id(),
option = option_kind
);
thread.update(cx, |thread, cx| {
thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
});
@@ -3128,7 +3147,7 @@ impl AcpThreadView {
.text_ui_sm(cx)
.h_full()
.children(terminal_view.map(|terminal_view| {
if terminal_view
let element = if terminal_view
.read(cx)
.content_mode(window, cx)
.is_scrollable()
@@ -3136,7 +3155,15 @@ impl AcpThreadView {
div().h_72().child(terminal_view).into_any_element()
} else {
terminal_view.into_any_element()
}
};
div()
.on_action(cx.listener(|_this, _: &NewTerminal, window, cx| {
window.dispatch_action(NewThread.boxed_clone(), cx);
cx.stop_propagation();
}))
.child(element)
.into_any_element()
})),
)
})
@@ -3588,6 +3615,7 @@ impl AcpThreadView {
) -> Option<AnyElement> {
let thread = thread_entity.read(cx);
let action_log = thread.action_log();
let telemetry = ActionLogTelemetry::from(thread);
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let plan = thread.plan();
@@ -3635,6 +3663,7 @@ impl AcpThreadView {
.when(self.edits_expanded, |parent| {
parent.child(self.render_edited_files(
action_log,
telemetry,
&changed_buffers,
pending_edits,
cx,
@@ -3915,6 +3944,7 @@ impl AcpThreadView {
fn render_edited_files(
&self,
action_log: &Entity<ActionLog>,
telemetry: ActionLogTelemetry,
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
pending_edits: bool,
cx: &Context<Self>,
@@ -4034,12 +4064,14 @@ impl AcpThreadView {
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log
.reject_edits_in_ranges(
buffer.clone(),
vec![Anchor::MIN..Anchor::MAX],
Some(telemetry.clone()),
cx,
)
.detach_and_log_err(cx);
@@ -4054,11 +4086,13 @@ impl AcpThreadView {
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer.clone(),
Anchor::MIN..Anchor::MAX,
Some(telemetry.clone()),
cx,
);
})
@@ -4169,6 +4203,8 @@ impl AcpThreadView {
.justify_between()
.child(
h_flex()
.gap_0p5()
.child(self.render_add_context_button(cx))
.child(self.render_follow_toggle(cx))
.children(self.render_burn_mode_toggle(cx)),
)
@@ -4274,17 +4310,23 @@ impl AcpThreadView {
let Some(thread) = self.thread() else {
return;
};
let telemetry = ActionLogTelemetry::from(thread.read(cx));
let action_log = thread.read(cx).action_log().clone();
action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
action_log.update(cx, |action_log, cx| {
action_log.keep_all_edits(Some(telemetry), cx)
});
}
fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
let Some(thread) = self.thread() else {
return;
};
let telemetry = ActionLogTelemetry::from(thread.read(cx));
let action_log = thread.read(cx).action_log().clone();
action_log
.update(cx, |action_log, cx| action_log.reject_all_edits(cx))
.update(cx, |action_log, cx| {
action_log.reject_all_edits(Some(telemetry), cx)
})
.detach();
}
@@ -4477,6 +4519,29 @@ impl AcpThreadView {
}))
}
fn render_add_context_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
let message_editor = self.message_editor.clone();
let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx);
IconButton::new("add-context", IconName::AtSign)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.when(!menu_visible, |this| {
this.tooltip(move |_window, cx| {
Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
})
})
.on_click(cx.listener(move |_this, _, window, cx| {
let message_editor_clone = message_editor.clone();
window.defer(cx, move |window, cx| {
message_editor_clone.update(cx, |message_editor, cx| {
message_editor.trigger_completion_menu(window, cx);
});
});
}))
}
fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
let workspace = self.workspace.clone();
MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
@@ -4680,35 +4745,36 @@ impl AcpThreadView {
.languages
.language_for_name("Markdown");
let (thread_summary, markdown) = if let Some(thread) = self.thread() {
let (thread_title, markdown) = if let Some(thread) = self.thread() {
let thread = thread.read(cx);
(thread.title().to_string(), thread.to_markdown(cx))
} else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().clone();
window.spawn(cx, async move |cx| {
let markdown_language = markdown_language_task.await?;
let buffer = project
.update(cx, |project, cx| project.create_buffer(false, cx))?
.await?;
buffer.update(cx, |buffer, cx| {
buffer.set_text(markdown, cx);
buffer.set_language(Some(markdown_language), cx);
buffer.set_capability(language::Capability::ReadOnly, cx);
})?;
workspace.update_in(cx, |workspace, window, cx| {
let project = workspace.project().clone();
if !project.read(cx).is_local() {
bail!("failed to open active thread as markdown in remote project");
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
});
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
});
let buffer = cx
.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone()));
workspace.add_item_to_active_pane(
Box::new(cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
editor.set_breadcrumb_header(thread_summary);
editor.set_breadcrumb_header(thread_title);
editor
})),
None,
@@ -4716,9 +4782,7 @@ impl AcpThreadView {
window,
cx,
);
anyhow::Ok(())
})??;
})?;
anyhow::Ok(())
})
}
@@ -5252,7 +5316,6 @@ impl AcpThreadView {
)
}
#[cfg(target_os = "windows")]
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
if self.show_codex_windows_warning {
Some(
@@ -5268,8 +5331,9 @@ impl AcpThreadView {
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.on_click(cx.listener({
move |_, _, window, cx| {
window.dispatch_action(
move |_, _, _window, cx| {
#[cfg(windows)]
_window.dispatch_action(
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
cx,
);
@@ -5295,9 +5359,9 @@ impl AcpThreadView {
}
}
fn render_thread_error(&self, cx: &mut Context<Self>) -> Option<Div> {
fn render_thread_error(&mut 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::Other(error) => self.render_any_thread_error(error.clone(), window, cx),
ThreadError::Refusal => self.render_refusal_error(cx),
ThreadError::AuthenticationRequired(error) => {
self.render_authentication_required_error(error.clone(), cx)
@@ -5344,20 +5408,31 @@ impl AcpThreadView {
)
}
fn get_current_model_name(&self, cx: &App) -> SharedString {
fn current_mode_id(&self, cx: &App) -> Option<Arc<str>> {
if let Some(thread) = self.as_native_thread(cx) {
Some(thread.read(cx).profile().0.clone())
} else if let Some(mode_selector) = self.mode_selector() {
Some(mode_selector.read(cx).mode().0)
} else {
None
}
}
fn current_model_id(&self, cx: &App) -> Option<String> {
self.model_selector
.as_ref()
.and_then(|selector| selector.read(cx).active_model(cx).map(|m| m.id.to_string()))
}
fn 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::<agent::NativeAgentServer>()
.is_some()
{
// Native agent - use the model name
if self.as_native_connection(cx).is_some() {
self.model_selector
.as_ref()
.and_then(|selector| selector.read(cx).active_model_name(cx))
.and_then(|selector| selector.read(cx).active_model(cx))
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("The model"))
} else {
// ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
@@ -5366,7 +5441,7 @@ impl AcpThreadView {
}
fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
let model_or_agent_name = self.get_current_model_name(cx);
let model_or_agent_name = self.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
@@ -5381,7 +5456,12 @@ impl AcpThreadView {
.dismiss_action(self.dismiss_error_button(cx))
}
fn render_any_thread_error(&self, error: SharedString, cx: &mut Context<'_, Self>) -> Callout {
fn render_any_thread_error(
&mut self,
error: SharedString,
window: &mut Window,
cx: &mut Context<'_, Self>,
) -> Callout {
let can_resume = self
.thread()
.map_or(false, |thread| thread.read(cx).can_resume(cx));
@@ -5394,11 +5474,24 @@ impl AcpThreadView {
supports_burn_mode && thread.completion_mode() == CompletionMode::Normal
});
let markdown = if let Some(markdown) = &self.thread_error_markdown {
markdown.clone()
} else {
let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx));
self.thread_error_markdown = Some(markdown.clone());
markdown
};
let markdown_style = default_markdown_style(false, true, window, cx);
let description = self
.render_markdown(markdown, markdown_style)
.into_any_element();
Callout::new()
.severity(Severity::Error)
.title("Error")
.icon(IconName::XCircle)
.description(error.clone())
.title("An Error Happened")
.description_slot(description)
.actions_slot(
h_flex()
.gap_0p5()
@@ -5417,11 +5510,9 @@ impl AcpThreadView {
})
.when(can_resume, |this| {
this.child(
Button::new("retry", "Retry")
.icon(IconName::RotateCw)
.icon_position(IconPosition::Start)
IconButton::new("retry", IconName::RotateCw)
.icon_size(IconSize::Small)
.label_size(LabelSize::Small)
.tooltip(Tooltip::text("Retry Generation"))
.on_click(cx.listener(|this, _, _window, cx| {
this.resume_chat(cx);
})),
@@ -5563,7 +5654,6 @@ impl AcpThreadView {
IconButton::new("copy", IconName::Copy)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Copy Error Message"))
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
@@ -5573,7 +5663,6 @@ impl AcpThreadView {
fn dismiss_error_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
IconButton::new("dismiss", IconName::Close)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(Tooltip::text("Dismiss Error"))
.on_click(cx.listener({
move |this, _, _, cx| {
@@ -5678,6 +5767,19 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement {
.into_any_element()
}
fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
if agent_name == "Zed Agent" {
format!("Message the {} — @ to include context", agent_name)
} else if has_commands {
format!(
"Message {} — @ to include context, / for commands",
agent_name
)
} else {
format!("Message {} — @ to include context", agent_name)
}
}
impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state {
@@ -5772,16 +5874,13 @@ impl Render for AcpThreadView {
})
.children(self.render_thread_retry_status_callout(window, cx))
.children({
#[cfg(target_os = "windows")]
{
if cfg!(windows) && self.project.read(cx).is_local() {
self.render_codex_windows_warning(cx)
}
#[cfg(not(target_os = "windows"))]
{
Vec::<Empty>::new()
} else {
None
}
})
.children(self.render_thread_error(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 { .. })
@@ -5847,7 +5946,6 @@ fn default_markdown_style(
syntax: cx.theme().syntax().clone(),
selection_background_color: colors.element_selection_background,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
heading_level_styles: Some(HeadingLevelStyles {
h1: Some(TextStyleRefinement {
font_size: Some(rems(1.15).into()),
@@ -5915,6 +6013,7 @@ fn default_markdown_style(
},
link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)),
color: Some(colors.text_accent),
underline: Some(UnderlineStyle {
color: Some(colors.text_accent.opacity(0.5)),
thickness: px(1.),
@@ -5967,7 +6066,6 @@ pub(crate) mod tests {
use acp_thread::StubAgentConnection;
use agent_client_protocol::SessionId;
use assistant_text_thread::TextThreadStore;
use editor::EditorSettings;
use fs::FakeFs;
use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
use project::Project;
@@ -6355,6 +6453,10 @@ pub(crate) mod tests {
struct SaboteurAgentConnection;
impl AgentConnection for SaboteurAgentConnection {
fn telemetry_id(&self) -> &'static str {
"saboteur"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
@@ -6415,6 +6517,10 @@ pub(crate) mod tests {
struct RefusalAgentConnection;
impl AgentConnection for RefusalAgentConnection {
fn telemetry_id(&self) -> &'static str {
"refusal"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
@@ -6477,13 +6583,8 @@ pub(crate) mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
prompt_store::init(cx)
});
}

View File

@@ -1,5 +1,5 @@
mod add_llm_provider_modal;
mod configure_context_server_modal;
pub mod configure_context_server_modal;
mod configure_context_server_tools_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -8,6 +8,7 @@ use std::{ops::Range, sync::Arc};
use agent::ContextServerRegistry;
use anyhow::Result;
use client::zed_urls;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap;
use context_server::ContextServerId;
@@ -26,26 +27,27 @@ use language_model::{
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
agent_server_store::{
AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
};
use settings::{Settings, SettingsStore, update_settings_file};
use ui::{
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
SwitchColor, Tooltip, WithScrollbar, prelude::*,
Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{Workspace, create_and_open_local_file};
use zed_actions::ExtensionCategoryFilter;
use zed_actions::{ExtensionCategoryFilter, OpenBrowser};
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{
AddContextServer,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
use crate::agent_configuration::add_llm_provider_modal::{
AddLlmProviderModal, LlmCompatibleProvider,
};
pub struct AgentConfiguration {
@@ -415,6 +417,7 @@ impl AgentConfiguration {
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(
Button::new("add-provider", "Add Provider")
@@ -425,7 +428,6 @@ impl AgentConfiguration {
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
let workspace = self.workspace.clone();
move |window, cx| {
@@ -447,6 +449,11 @@ impl AgentConfiguration {
})
}))
}
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
@@ -541,12 +548,13 @@ impl AgentConfiguration {
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.anchor(gpui::Corner::TopRight)
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Add Custom Server", None, {
|window, cx| window.dispatch_action(AddContextServer.boxed_clone(), cx)
|window, cx| {
window.dispatch_action(crate::AddContextServer.boxed_clone(), cx)
}
})
.entry("Install from Extensions", None, {
|window, cx| {
@@ -564,6 +572,11 @@ impl AgentConfiguration {
})
}))
}
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
@@ -638,15 +651,13 @@ impl AgentConfiguration {
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
let is_from_extension = server_configuration
.as_ref()
.map(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
})
.unwrap_or(false);
// Servers without a configuration can only be provided by extensions.
let provided_by_extension = server_configuration.as_ref().is_none_or(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
});
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -660,7 +671,7 @@ impl AgentConfiguration {
.tools_for_server(&context_server_id)
.count();
let (source_icon, source_tooltip) = if is_from_extension {
let (source_icon, source_tooltip) = if provided_by_extension {
(
IconName::ZedSrcExtension,
"This MCP server was installed from an extension.",
@@ -697,7 +708,10 @@ impl AgentConfiguration {
"Server is stopped.",
),
};
let is_remote = server_configuration
.as_ref()
.map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. }))
.unwrap_or(false);
let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
.trigger_with_tooltip(
IconButton::new("context-server-config-menu", IconName::Settings)
@@ -710,7 +724,6 @@ impl AgentConfiguration {
let fs = self.fs.clone();
let context_server_id = context_server_id.clone();
let language_registry = self.language_registry.clone();
let context_server_store = self.context_server_store.clone();
let workspace = self.workspace.clone();
let context_server_registry = self.context_server_registry.clone();
@@ -721,14 +734,25 @@ impl AgentConfiguration {
let language_registry = language_registry.clone();
let workspace = workspace.clone();
move |window, cx| {
ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach_and_log_err(cx);
if is_remote {
crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach();
} else {
ConfigureContextServerModal::show_modal_for_existing_server(
context_server_id.clone(),
language_registry.clone(),
workspace.clone(),
window,
cx,
)
.detach();
}
}
}).when(tool_count > 0, |this| this.entry("View Tools", None, {
let context_server_id = context_server_id.clone();
@@ -752,23 +776,10 @@ impl AgentConfiguration {
.entry("Uninstall", None, {
let fs = fs.clone();
let context_server_id = context_server_id.clone();
let context_server_store = context_server_store.clone();
let workspace = workspace.clone();
move |_, cx| {
let is_provided_by_extension = context_server_store
.read(cx)
.configuration_for_server(&context_server_id)
.as_ref()
.map(|config| {
matches!(
config.as_ref(),
ContextServerConfiguration::Extension { .. }
)
})
.unwrap_or(false);
let uninstall_extension_task = match (
is_provided_by_extension,
provided_by_extension,
resolve_extension_for_context_server(&context_server_id, cx),
) {
(true, Some((id, manifest))) => {
@@ -959,7 +970,7 @@ impl AgentConfiguration {
.cloned()
.collect::<Vec<_>>();
let user_defined_agents = user_defined_agents
let user_defined_agents: Vec<_> = user_defined_agents
.into_iter()
.map(|name| {
let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
@@ -967,27 +978,93 @@ impl AgentConfiguration {
} else {
AgentIcon::Name(IconName::Ai)
};
self.render_agent_server(icon, name, true)
.into_any_element()
(name, icon)
})
.collect::<Vec<_>>();
.collect();
let add_agens_button = Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small)
.on_click(move |_, window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
.trigger(
Button::new("add-agent", "Add Agent")
.style(ButtonStyle::Outlined)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.label_size(LabelSize::Small),
)
.menu({
move |window, cx| {
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
menu.entry("Install from Extensions", None, {
|window, cx| {
window.dispatch_action(
zed_actions::Extensions {
category_filter: Some(
ExtensionCategoryFilter::AgentServers,
),
id: None,
}
.boxed_clone(),
cx,
)
}
})
.detach_and_log_err(cx);
.entry("Add Custom Agent", None, {
move |window, cx| {
if let Some(workspace) = window.root().flatten() {
let workspace = workspace.downgrade();
window
.spawn(cx, async |cx| {
open_new_agent_servers_entry_in_settings_editor(
workspace, cx,
)
.await
})
.detach_and_log_err(cx);
}
}
})
.separator()
.header("Learn More")
.item(
ContextMenuEntry::new("Agent Servers Docs")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler({
move |window, cx| {
window.dispatch_action(
Box::new(OpenBrowser {
url: zed_urls::agent_server_docs(cx),
}),
cx,
);
}
}),
)
.item(
ContextMenuEntry::new("ACP Docs")
.icon(IconName::ArrowUpRight)
.icon_color(Color::Muted)
.icon_position(IconPosition::End)
.handler({
move |window, cx| {
window.dispatch_action(
Box::new(OpenBrowser {
url: "https://agentclientprotocol.com/".into(),
}),
cx,
);
}
}),
)
}))
}
})
.anchor(gpui::Corner::TopRight)
.offset(gpui::Point {
x: px(0.0),
y: px(2.0),
});
v_flex()
@@ -998,7 +1075,7 @@ impl AgentConfiguration {
.child(self.render_section_title(
"External Agents",
"All agents connected through the Agent Client Protocol.",
add_agens_button.into_any_element(),
add_agent_popover.into_any_element(),
))
.child(
v_flex()
@@ -1009,26 +1086,29 @@ impl AgentConfiguration {
AgentIcon::Name(IconName::AiClaude),
"Claude Code",
false,
cx,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiOpenAi),
"Codex CLI",
false,
cx,
))
.child(Divider::horizontal().color(DividerColor::BorderFaded))
.child(self.render_agent_server(
AgentIcon::Name(IconName::AiGemini),
"Gemini CLI",
false,
cx,
))
.map(|mut parent| {
for agent in user_defined_agents {
for (name, icon) in user_defined_agents {
parent = parent
.child(
Divider::horizontal().color(DividerColor::BorderFaded),
)
.child(agent);
.child(self.render_agent_server(icon, name, true, cx));
}
parent
}),
@@ -1041,13 +1121,14 @@ impl AgentConfiguration {
icon: AgentIcon,
name: impl Into<SharedString>,
external: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
let name = name.into();
let icon = match icon {
AgentIcon::Name(icon_name) => Icon::new(icon_name)
.size(IconSize::Small)
.color(Color::Muted),
AgentIcon::Path(icon_path) => Icon::from_path(icon_path)
AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path)
.size(IconSize::Small)
.color(Color::Muted),
};
@@ -1055,28 +1136,53 @@ impl AgentConfiguration {
let tooltip_id = SharedString::new(format!("agent-source-{}", name));
let tooltip_message = format!("The {} agent was installed from an extension.", name);
let agent_server_name = ExternalAgentServerName(name.clone());
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Uninstall Agent Extension"))
.on_click(cx.listener(move |this, _, _window, cx| {
let agent_name = agent_server_name.clone();
if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
store.get_extension_id_for_agent(&agent_name)
}) {
ExtensionStore::global(cx)
.update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
.detach_and_log_err(cx);
}
}));
h_flex()
.gap_1p5()
.child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.gap_1()
.justify_between()
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
h_flex()
.gap_1p5()
.child(icon)
.child(Label::new(name))
.when(external, |this| {
this.child(
div()
.id(tooltip_id)
.flex_none()
.tooltip(Tooltip::text(tooltip_message))
.child(
Icon::new(IconName::ZedSrcExtension)
.size(IconSize::Small)
.color(Color::Muted),
),
)
})
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::Small),
),
)
.when(external, |this| this.child(uninstall_button))
}
}

View File

@@ -3,16 +3,42 @@ use std::sync::Arc;
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
use gpui::{
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task,
};
use language_model::LanguageModelRegistry;
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
use ui::{
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState,
WithScrollbar, prelude::*,
};
use ui_input::InputField;
use workspace::{ModalView, Workspace};
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
tab_index: isize,
window: &mut Window,
cx: &mut App,
) -> Entity<InputField> {
cx.new(|cx| {
let input = InputField::new(window, cx, placeholder)
.label(label)
.tab_index(tab_index)
.tab_stop(true);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
#[derive(Clone, Copy)]
pub enum LlmCompatibleProvider {
OpenAi,
@@ -41,12 +67,14 @@ struct AddLlmProviderInput {
impl AddLlmProviderInput {
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
let provider_name =
single_line_input("Provider Name", provider.name(), None, 1, window, cx);
let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx);
let api_key = single_line_input(
"API Key",
"000000000000000000000000000000000000000000000000",
None,
3,
window,
cx,
);
@@ -55,12 +83,13 @@ impl AddLlmProviderInput {
provider_name,
api_url,
api_key,
models: vec![ModelInput::new(window, cx)],
models: vec![ModelInput::new(0, window, cx)],
}
}
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
self.models.push(ModelInput::new(window, cx));
let model_index = self.models.len();
self.models.push(ModelInput::new(model_index, window, cx));
}
fn remove_model(&mut self, index: usize) {
@@ -84,11 +113,14 @@ struct ModelInput {
}
impl ModelInput {
fn new(window: &mut Window, cx: &mut App) -> Self {
fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self {
let base_tab_index = (3 + (model_index * 4)) as isize;
let model_name = single_line_input(
"Model Name",
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
None,
base_tab_index + 1,
window,
cx,
);
@@ -96,6 +128,7 @@ impl ModelInput {
"Max Completion Tokens",
"200000",
Some("200000"),
base_tab_index + 2,
window,
cx,
);
@@ -103,16 +136,26 @@ impl ModelInput {
"Max Output Tokens",
"Max Output Tokens",
Some("32000"),
base_tab_index + 3,
window,
cx,
);
let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
let max_tokens = single_line_input(
"Max Tokens",
"Max Tokens",
Some("200000"),
base_tab_index + 4,
window,
cx,
);
let ModelCapabilities {
tools,
images,
parallel_tool_calls,
prompt_cache_key,
} = ModelCapabilities::default();
Self {
name: model_name,
max_completion_tokens,
@@ -165,24 +208,6 @@ impl ModelInput {
}
}
fn single_line_input(
label: impl Into<SharedString>,
placeholder: impl Into<SharedString>,
text: Option<&str>,
window: &mut Window,
cx: &mut App,
) -> Entity<InputField> {
cx.new(|cx| {
let input = InputField::new(window, cx, placeholder).label(label);
if let Some(text) = text {
input
.editor()
.update(cx, |editor, cx| editor.set_text(text, window, cx));
}
input
})
}
fn save_provider_to_settings(
input: &AddLlmProviderInput,
cx: &mut App,
@@ -258,6 +283,7 @@ fn save_provider_to_settings(
pub struct AddLlmProviderModal {
provider: LlmCompatibleProvider,
input: AddLlmProviderInput,
scroll_handle: ScrollHandle,
focus_handle: FocusHandle,
last_error: Option<SharedString>,
}
@@ -278,6 +304,7 @@ impl AddLlmProviderModal {
provider,
last_error: None,
focus_handle: cx.focus_handle(),
scroll_handle: ScrollHandle::new(),
}
}
@@ -418,6 +445,19 @@ impl AddLlmProviderModal {
)
})
}
fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
window.focus_next();
}
fn on_tab_prev(
&mut self,
_: &menu::SelectPrevious,
window: &mut Window,
_: &mut Context<Self>,
) {
window.focus_prev();
}
}
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
@@ -431,15 +471,27 @@ impl Focusable for AddLlmProviderModal {
impl ModalView for AddLlmProviderModal {}
impl Render for AddLlmProviderModal {
fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
div()
let window_size = window.viewport_size();
let rem_size = window.rem_size();
let is_large_window = window_size.height / rem_size > rems_from_px(600.).0;
let modal_max_height = if is_large_window {
rems_from_px(450.)
} else {
rems_from_px(200.)
};
v_flex()
.id("add-llm-provider-modal")
.key_context("AddLlmProviderModal")
.w(rems(34.))
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::on_tab))
.on_action(cx.listener(Self::on_tab_prev))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
@@ -462,17 +514,25 @@ impl Render for AddLlmProviderModal {
)
})
.child(
v_flex()
.id("modal_content")
div()
.size_full()
.max_h_128()
.overflow_y_scroll()
.px(DynamicSpacing::Base12.rems(cx))
.gap(DynamicSpacing::Base04.rems(cx))
.child(self.input.provider_name.clone())
.child(self.input.api_url.clone())
.child(self.input.api_key.clone())
.child(self.render_model_section(cx)),
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.child(
v_flex()
.id("modal_content")
.size_full()
.tab_group()
.max_h(modal_max_height)
.pl_3()
.pr_4()
.gap_2()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.child(self.input.provider_name.clone())
.child(self.input.api_url.clone())
.child(self.input.api_key.clone())
.child(self.render_model_section(cx)),
),
)
.footer(
ModalFooter::new().end_slot(
@@ -515,16 +575,14 @@ impl Render for AddLlmProviderModal {
#[cfg(test)]
mod tests {
use super::*;
use editor::EditorSettings;
use fs::FakeFs;
use gpui::{TestAppContext, VisualTestContext};
use language::language_settings;
use language_model::{
LanguageModelProviderId, LanguageModelProviderName,
fake_provider::FakeLanguageModelProvider,
};
use project::Project;
use settings::{Settings as _, SettingsStore};
use settings::SettingsStore;
use util::path;
#[gpui::test]
@@ -644,7 +702,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
let model_input = ModelInput::new(window, cx);
let model_input = ModelInput::new(0, window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -680,7 +738,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
let mut model_input = ModelInput::new(window, cx);
let mut model_input = ModelInput::new(0, window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -705,7 +763,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
let mut model_input = ModelInput::new(window, cx);
let mut model_input = ModelInput::new(0, window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -730,13 +788,9 @@ mod tests {
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
workspace::init_settings(cx);
Project::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
language_settings::init(cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
language_models::init_settings(cx);
});
let fs = FakeFs::new(cx.executor());
@@ -773,7 +827,7 @@ mod tests {
models.iter().enumerate()
{
if i >= input.models.len() {
input.models.push(ModelInput::new(window, cx));
input.models.push(ModelInput::new(i, window, cx));
}
let model = &mut input.models[i];
set_text(&model.name, name, window, cx);

View File

@@ -4,11 +4,12 @@ use std::{
};
use anyhow::{Context as _, Result};
use collections::HashMap;
use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
};
use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
@@ -20,10 +21,12 @@ use project::{
project_settings::{ContextServerSettings, ProjectSettings},
worktree_store::WorktreeStore,
};
use serde::Deserialize;
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
use ui::{
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*,
CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip,
WithScrollbar, prelude::*,
};
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
@@ -36,6 +39,11 @@ enum ConfigurationTarget {
id: ContextServerId,
command: ContextServerCommand,
},
ExistingHttp {
id: ContextServerId,
url: String,
headers: HashMap<String, String>,
},
Extension {
id: ContextServerId,
repository_url: Option<SharedString>,
@@ -46,9 +54,11 @@ enum ConfigurationTarget {
enum ConfigurationSource {
New {
editor: Entity<Editor>,
is_http: bool,
},
Existing {
editor: Entity<Editor>,
is_http: bool,
},
Extension {
id: ContextServerId,
@@ -96,6 +106,7 @@ impl ConfigurationSource {
match target {
ConfigurationTarget::New => ConfigurationSource::New {
editor: create_editor(context_server_input(None), jsonc_language, window, cx),
is_http: false,
},
ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
editor: create_editor(
@@ -104,6 +115,20 @@ impl ConfigurationSource {
window,
cx,
),
is_http: false,
},
ConfigurationTarget::ExistingHttp {
id,
url,
headers: auth,
} => ConfigurationSource::Existing {
editor: create_editor(
context_server_http_input(Some((id, url, auth))),
jsonc_language,
window,
cx,
),
is_http: true,
},
ConfigurationTarget::Extension {
id,
@@ -140,16 +165,30 @@ impl ConfigurationSource {
fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
match self {
ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
enabled: true,
command,
},
)
})
ConfigurationSource::New { editor, is_http }
| ConfigurationSource::Existing { editor, is_http } => {
if *is_http {
parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| {
(
id,
ContextServerSettings::Http {
enabled: true,
url,
headers: auth,
},
)
})
} else {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
enabled: true,
command,
},
)
})
}
}
ConfigurationSource::Extension {
id,
@@ -211,6 +250,66 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
)
}
fn context_server_http_input(
existing: Option<(ContextServerId, String, HashMap<String, String>)>,
) -> String {
let (name, url, headers) = match existing {
Some((id, url, headers)) => {
let header = if headers.is_empty() {
r#"// "Authorization": "Bearer <token>"#.to_string()
} else {
let json = serde_json::to_string_pretty(&headers).unwrap();
let mut lines = json.split("\n").collect::<Vec<_>>();
if lines.len() > 1 {
lines.remove(0);
lines.pop();
}
lines
.into_iter()
.map(|line| format!(" {}", line))
.collect::<String>()
};
(id.0.to_string(), url, header)
}
None => (
"some-remote-server".to_string(),
"https://example.com/mcp".to_string(),
r#"// "Authorization": "Bearer <token>"#.to_string(),
),
};
format!(
r#"{{
/// The name of your remote MCP server
"{name}": {{
/// The URL of the remote MCP server
"url": "{url}",
"headers": {{
/// Any headers to send along
{headers}
}}
}}
}}"#
)
}
fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap<String, String>)> {
#[derive(Deserialize)]
struct Temp {
url: String,
#[serde(default)]
headers: HashMap<String, String>,
}
let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
if value.len() != 1 {
anyhow::bail!("Expected exactly one context server configuration");
}
let (key, value) = value.into_iter().next().unwrap();
Ok((ContextServerId(key.into()), value.url, value.headers))
}
fn resolve_context_server_extension(
id: ContextServerId,
worktree_store: Entity<WorktreeStore>,
@@ -252,6 +351,7 @@ pub struct ConfigureContextServerModal {
source: ConfigurationSource,
state: State,
original_server_id: Option<ContextServerId>,
scroll_handle: ScrollHandle,
}
impl ConfigureContextServerModal {
@@ -310,6 +410,15 @@ impl ConfigureContextServerModal {
id: server_id,
command,
}),
ContextServerSettings::Http {
enabled: _,
url,
headers,
} => Some(ConfigurationTarget::ExistingHttp {
id: server_id,
url,
headers,
}),
ContextServerSettings::Extension { .. } => {
match workspace
.update(cx, |workspace, cx| {
@@ -351,6 +460,7 @@ impl ConfigureContextServerModal {
state: State::Idle,
original_server_id: match &target {
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
ConfigurationTarget::New => None,
},
@@ -361,6 +471,7 @@ impl ConfigureContextServerModal {
window,
cx,
),
scroll_handle: ScrollHandle::new(),
})
})
})
@@ -478,7 +589,7 @@ impl ModalView for ConfigureContextServerModal {}
impl Focusable for ConfigureContextServerModal {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.source {
ConfigurationSource::New { editor } => editor.focus_handle(cx),
ConfigurationSource::New { editor, .. } => editor.focus_handle(cx),
ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
ConfigurationSource::Extension { editor, .. } => editor
.as_ref()
@@ -524,9 +635,10 @@ impl ConfigureContextServerModal {
}
fn render_modal_content(&self, cx: &App) -> AnyElement {
// All variants now use single editor approach
let editor = match &self.source {
ConfigurationSource::New { editor } => editor,
ConfigurationSource::Existing { editor } => editor,
ConfigurationSource::New { editor, .. } => editor,
ConfigurationSource::Existing { editor, .. } => editor,
ConfigurationSource::Extension { editor, .. } => {
let Some(editor) = editor else {
return div().into_any_element();
@@ -598,6 +710,36 @@ impl ConfigureContextServerModal {
move |_, _, cx| cx.open_url(&repository_url)
}),
)
} else if let ConfigurationSource::New { is_http, .. } = &self.source {
let label = if *is_http {
"Run command"
} else {
"Connect via HTTP"
};
let tooltip = if *is_http {
"Configure an MCP serevr that runs on stdin/stdout."
} else {
"Configure an MCP server that you connect to over HTTP"
};
Some(
Button::new("toggle-kind", label)
.tooltip(Tooltip::text(tooltip))
.on_click(cx.listener(|this, _, window, cx| match &mut this.source {
ConfigurationSource::New { editor, is_http } => {
*is_http = !*is_http;
let new_text = if *is_http {
context_server_http_input(None)
} else {
context_server_input(None)
};
editor.update(cx, |editor, cx| {
editor.set_text(new_text, window, cx);
})
}
_ => {}
})),
)
} else {
None
},
@@ -680,6 +822,7 @@ impl ConfigureContextServerModal {
impl Render for ConfigureContextServerModal {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let scroll_handle = self.scroll_handle.clone();
div()
.elevation_3(cx)
.w(rems(34.))
@@ -699,14 +842,29 @@ impl Render for ConfigureContextServerModal {
Modal::new("configure-context-server", None)
.header(self.render_modal_header())
.section(
Section::new()
.child(self.render_modal_description(window, cx))
.child(self.render_modal_content(cx))
.child(match &self.state {
State::Idle => div(),
State::Waiting => Self::render_waiting_for_context_server(),
State::Error(error) => Self::render_modal_error(error.clone()),
}),
Section::new().child(
div()
.size_full()
.child(
div()
.id("modal-content")
.max_h(vh(0.7, window))
.overflow_y_scroll()
.track_scroll(&scroll_handle)
.child(self.render_modal_description(window, cx))
.child(self.render_modal_content(cx))
.child(match &self.state {
State::Idle => div(),
State::Waiting => {
Self::render_waiting_for_context_server()
}
State::Error(error) => {
Self::render_modal_error(error.clone())
}
}),
)
.vertical_scrollbar_for(scroll_handle, window, cx),
),
)
.footer(self.render_modal_footer(cx)),
)

View File

@@ -7,8 +7,10 @@ use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profil
use editor::Editor;
use fs::Fs;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
use language_model::LanguageModel;
use settings::Settings as _;
use language_model::{LanguageModel, LanguageModelRegistry};
use settings::{
LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
};
use ui::{
KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
};
@@ -16,6 +18,7 @@ use workspace::{ModalView, Workspace};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::language_model_selector::{LanguageModelSelector, language_model_selector};
use crate::{AgentPanel, ManageProfiles};
enum Mode {
@@ -32,6 +35,11 @@ enum Mode {
tool_picker: Entity<ToolPicker>,
_subscription: Subscription,
},
ConfigureDefaultModel {
profile_id: AgentProfileId,
model_picker: Entity<LanguageModelSelector>,
_subscription: Subscription,
},
}
impl Mode {
@@ -83,6 +91,7 @@ pub struct ChooseProfileMode {
pub struct ViewProfileMode {
profile_id: AgentProfileId,
fork_profile: NavigableEntry,
configure_default_model: NavigableEntry,
configure_tools: NavigableEntry,
configure_mcps: NavigableEntry,
cancel_item: NavigableEntry,
@@ -180,6 +189,7 @@ impl ManageProfilesModal {
self.mode = Mode::ViewProfile(ViewProfileMode {
profile_id,
fork_profile: NavigableEntry::focusable(cx),
configure_default_model: NavigableEntry::focusable(cx),
configure_tools: NavigableEntry::focusable(cx),
configure_mcps: NavigableEntry::focusable(cx),
cancel_item: NavigableEntry::focusable(cx),
@@ -187,6 +197,83 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
fn configure_default_model(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
cx: &mut Context<Self>,
) {
let fs = self.fs.clone();
let profile_id_for_closure = profile_id.clone();
let model_picker = cx.new(|cx| {
let fs = fs.clone();
let profile_id = profile_id_for_closure.clone();
language_model_selector(
{
let profile_id = profile_id.clone();
move |cx| {
let settings = AgentSettings::get_global(cx);
settings
.profiles
.get(&profile_id)
.and_then(|profile| profile.default_model.as_ref())
.and_then(|selection| {
let registry = LanguageModelRegistry::read_global(cx);
let provider_id = language_model::LanguageModelProviderId(
gpui::SharedString::from(selection.provider.0.clone()),
);
let provider = registry.provider(&provider_id)?;
let model = provider
.provided_models(cx)
.iter()
.find(|m| m.id().0 == selection.model.as_str())?
.clone();
Some(language_model::ConfiguredModel { provider, model })
})
}
},
move |model, cx| {
let provider = model.provider_id().0.to_string();
let model_id = model.id().0.to_string();
let profile_id = profile_id.clone();
update_settings_file(fs.clone(), cx, move |settings, _cx| {
let agent_settings = settings.agent.get_or_insert_default();
if let Some(profiles) = agent_settings.profiles.as_mut() {
if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
profile.default_model = Some(LanguageModelSelection {
provider: LanguageModelProviderSetting(provider.clone()),
model: model_id.clone(),
});
}
}
});
},
false, // Do not use popover styles for the model picker
window,
cx,
)
.modal(false)
});
let dismiss_subscription = cx.subscribe_in(&model_picker, window, {
let profile_id = profile_id.clone();
move |this, _picker, _: &DismissEvent, window, cx| {
this.view_profile(profile_id.clone(), window, cx);
}
});
self.mode = Mode::ConfigureDefaultModel {
profile_id,
model_picker,
_subscription: dismiss_subscription,
};
self.focus_handle(cx).focus(window);
}
fn configure_mcp_tools(
&mut self,
profile_id: AgentProfileId,
@@ -277,6 +364,7 @@ impl ManageProfilesModal {
Mode::ViewProfile(_) => {}
Mode::ConfigureTools { .. } => {}
Mode::ConfigureMcps { .. } => {}
Mode::ConfigureDefaultModel { .. } => {}
}
}
@@ -299,6 +387,9 @@ impl ManageProfilesModal {
Mode::ConfigureMcps { profile_id, .. } => {
self.view_profile(profile_id.clone(), window, cx)
}
Mode::ConfigureDefaultModel { profile_id, .. } => {
self.view_profile(profile_id.clone(), window, cx)
}
}
}
}
@@ -313,6 +404,7 @@ impl Focusable for ManageProfilesModal {
Mode::ViewProfile(_) => self.focus_handle.clone(),
Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
Mode::ConfigureDefaultModel { model_picker, .. } => model_picker.focus_handle(cx),
}
}
}
@@ -544,6 +636,47 @@ impl ManageProfilesModal {
}),
),
)
.child(
div()
.id("configure-default-model")
.track_focus(&mode.configure_default_model.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_default_model(
profile_id.clone(),
window,
cx,
);
})
})
.child(
ListItem::new("model-item")
.toggle_state(
mode.configure_default_model
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.start_slot(
Icon::new(IconName::ZedAssistant)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure Default Model"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_default_model(
profile_id.clone(),
window,
cx,
);
})
}),
),
)
.child(
div()
.id("configure-builtin-tools")
@@ -668,6 +801,7 @@ impl ManageProfilesModal {
.into_any_element(),
)
.entry(mode.fork_profile)
.entry(mode.configure_default_model)
.entry(mode.configure_tools)
.entry(mode.configure_mcps)
.entry(mode.cancel_item)
@@ -753,6 +887,29 @@ impl Render for ManageProfilesModal {
.child(go_back_item)
.into_any_element()
}
Mode::ConfigureDefaultModel {
profile_id,
model_picker,
..
} => {
let profile_name = settings
.profiles
.get(profile_id)
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure Default Model"),
Some(IconName::Ai),
))
.child(ListSeparator)
.child(v_flex().w(rems(34.)).child(model_picker.clone()))
.child(ListSeparator)
.child(go_back_item)
.into_any_element()
}
Mode::ConfigureMcps {
profile_id,
tool_picker,

View File

@@ -314,6 +314,7 @@ impl PickerDelegate for ToolPickerDelegate {
)
})
.collect(),
default_model: default_profile.default_model.clone(),
});
if let Some(server_id) = server_id {

View File

@@ -1,6 +1,6 @@
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
use acp_thread::{AcpThread, AcpThreadEvent};
use action_log::ActionLog;
use action_log::ActionLogTelemetry;
use agent_settings::AgentSettings;
use anyhow::Result;
use buffer_diff::DiffHunkStatus;
@@ -40,79 +40,16 @@ use zed_actions::assistant::ToggleFocus;
pub struct AgentDiffPane {
multibuffer: Entity<MultiBuffer>,
editor: Entity<Editor>,
thread: AgentDiffThread,
thread: Entity<AcpThread>,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
title: SharedString,
_subscriptions: Vec<Subscription>,
}
#[derive(PartialEq, Eq, Clone)]
pub enum AgentDiffThread {
AcpThread(Entity<AcpThread>),
}
impl AgentDiffThread {
fn project(&self, cx: &App) -> Entity<Project> {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
}
}
fn action_log(&self, cx: &App) -> Entity<ActionLog> {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
}
}
fn title(&self, cx: &App) -> SharedString {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
}
}
fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
match self {
AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
}
}
fn downgrade(&self) -> WeakAgentDiffThread {
match self {
AgentDiffThread::AcpThread(thread) => {
WeakAgentDiffThread::AcpThread(thread.downgrade())
}
}
}
}
impl From<Entity<AcpThread>> for AgentDiffThread {
fn from(entity: Entity<AcpThread>) -> Self {
AgentDiffThread::AcpThread(entity)
}
}
#[derive(PartialEq, Eq, Clone)]
pub enum WeakAgentDiffThread {
AcpThread(WeakEntity<AcpThread>),
}
impl WeakAgentDiffThread {
pub fn upgrade(&self) -> Option<AgentDiffThread> {
match self {
WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
}
}
}
impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
fn from(entity: WeakEntity<AcpThread>) -> Self {
WeakAgentDiffThread::AcpThread(entity)
}
}
impl AgentDiffPane {
pub fn deploy(
thread: impl Into<AgentDiffThread>,
thread: Entity<AcpThread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut App,
@@ -123,12 +60,11 @@ impl AgentDiffPane {
}
pub fn deploy_in_workspace(
thread: impl Into<AgentDiffThread>,
thread: Entity<AcpThread>,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
let thread = thread.into();
let existing_diff = workspace
.items_of_type::<AgentDiffPane>(cx)
.find(|diff| diff.read(cx).thread == thread);
@@ -145,7 +81,7 @@ impl AgentDiffPane {
}
pub fn new(
thread: AgentDiffThread,
thread: Entity<AcpThread>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -153,7 +89,7 @@ impl AgentDiffPane {
let focus_handle = cx.focus_handle();
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
let project = thread.project(cx);
let project = thread.read(cx).project().clone();
let editor = cx.new(|cx| {
let mut editor =
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -164,19 +100,16 @@ impl AgentDiffPane {
editor
});
let action_log = thread.action_log(cx);
let action_log = thread.read(cx).action_log().clone();
let mut this = Self {
_subscriptions: vec![
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
this.update_excerpts(window, cx)
}),
match &thread {
AgentDiffThread::AcpThread(thread) => cx
.subscribe(thread, |this, _thread, event, cx| {
this.handle_acp_thread_event(event, cx)
}),
},
cx.subscribe(&thread, |this, _thread, event, cx| {
this.handle_acp_thread_event(event, cx)
}),
],
title: SharedString::default(),
multibuffer,
@@ -191,7 +124,12 @@ impl AgentDiffPane {
}
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
let changed_buffers = self
.thread
.read(cx)
.action_log()
.read(cx)
.changed_buffers(cx);
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
for (buffer, diff_handle) in changed_buffers {
@@ -278,7 +216,7 @@ impl AgentDiffPane {
}
fn update_title(&mut self, cx: &mut Context<Self>) {
let new_title = self.thread.title(cx);
let new_title = self.thread.read(cx).title();
if new_title != self.title {
self.title = new_title;
cx.emit(EditorEvent::TitleChanged);
@@ -340,16 +278,18 @@ impl AgentDiffPane {
}
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
self.thread
.action_log(cx)
.update(cx, |action_log, cx| action_log.keep_all_edits(cx))
let telemetry = ActionLogTelemetry::from(self.thread.read(cx));
let action_log = self.thread.read(cx).action_log().clone();
action_log.update(cx, |action_log, cx| {
action_log.keep_all_edits(Some(telemetry), cx)
});
}
}
fn keep_edits_in_selection(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -364,7 +304,7 @@ fn keep_edits_in_selection(
fn reject_edits_in_selection(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut Context<Editor>,
) {
@@ -378,7 +318,7 @@ fn reject_edits_in_selection(
fn keep_edits_in_ranges(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
ranges: Vec<Range<editor::Anchor>>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -393,8 +333,15 @@ fn keep_edits_in_ranges(
for hunk in &diff_hunks_in_ranges {
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
if let Some(buffer) = buffer {
thread.action_log(cx).update(cx, |action_log, cx| {
action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
let action_log = thread.read(cx).action_log().clone();
let telemetry = ActionLogTelemetry::from(thread.read(cx));
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer,
hunk.buffer_range.clone(),
Some(telemetry),
cx,
)
});
}
}
@@ -403,7 +350,7 @@ fn keep_edits_in_ranges(
fn reject_edits_in_ranges(
editor: &mut Editor,
buffer_snapshot: &MultiBufferSnapshot,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
ranges: Vec<Range<editor::Anchor>>,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -427,11 +374,12 @@ fn reject_edits_in_ranges(
}
}
let action_log = thread.read(cx).action_log().clone();
let telemetry = ActionLogTelemetry::from(thread.read(cx));
for (buffer, ranges) in ranges_by_buffer {
thread
.action_log(cx)
action_log
.update(cx, |action_log, cx| {
action_log.reject_edits_in_ranges(buffer, ranges, cx)
action_log.reject_edits_in_ranges(buffer, ranges, Some(telemetry.clone()), cx)
})
.detach_and_log_err(cx);
}
@@ -531,7 +479,7 @@ impl Item for AgentDiffPane {
}
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
let title = self.thread.title(cx);
let title = self.thread.read(cx).title();
Label::new(format!("Review: {}", title))
.color(if params.selected {
Color::Default
@@ -712,7 +660,7 @@ impl Render for AgentDiffPane {
}
}
fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
fn diff_hunk_controls(thread: &Entity<AcpThread>) -> editor::RenderDiffHunkControlsFn {
let thread = thread.clone();
Arc::new(
@@ -739,7 +687,7 @@ fn render_diff_hunk_controls(
hunk_range: Range<editor::Anchor>,
is_created_file: bool,
line_height: Pixels,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
editor: &Entity<Editor>,
cx: &mut App,
) -> AnyElement {
@@ -1153,8 +1101,11 @@ impl Render for AgentDiffToolbar {
return Empty.into_any();
};
let has_pending_edit_tool_use =
agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
let has_pending_edit_tool_use = agent_diff
.read(cx)
.thread
.read(cx)
.has_pending_edit_tool_calls();
if has_pending_edit_tool_use {
return div().px_2().child(spinner_icon).into_any();
@@ -1214,7 +1165,7 @@ pub enum EditorState {
}
struct WorkspaceThread {
thread: WeakAgentDiffThread,
thread: WeakEntity<AcpThread>,
_thread_subscriptions: (Subscription, Subscription),
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
_settings_subscription: Subscription,
@@ -1239,23 +1190,23 @@ impl AgentDiff {
pub fn set_active_thread(
workspace: &WeakEntity<Workspace>,
thread: impl Into<AgentDiffThread>,
thread: Entity<AcpThread>,
window: &mut Window,
cx: &mut App,
) {
Self::global(cx).update(cx, |this, cx| {
this.register_active_thread_impl(workspace, thread.into(), window, cx);
this.register_active_thread_impl(workspace, thread, window, cx);
});
}
fn register_active_thread_impl(
&mut self,
workspace: &WeakEntity<Workspace>,
thread: AgentDiffThread,
thread: Entity<AcpThread>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let action_log = thread.action_log(cx);
let action_log = thread.read(cx).action_log().clone();
let action_log_subscription = cx.observe_in(&action_log, window, {
let workspace = workspace.clone();
@@ -1264,14 +1215,12 @@ impl AgentDiff {
}
});
let thread_subscription = match &thread {
AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
let workspace = workspace.clone();
move |this, thread, event, window, cx| {
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
}
}),
};
let thread_subscription = cx.subscribe_in(&thread, window, {
let workspace = workspace.clone();
move |this, thread, event, window, cx| {
this.handle_acp_thread_event(&workspace, thread, event, window, cx)
}
});
if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
// replace thread and action log subscription, but keep editors
@@ -1348,7 +1297,7 @@ impl AgentDiff {
fn register_review_action<T: Action>(
workspace: &mut Workspace,
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState
+ 'static,
this: &Entity<AgentDiff>,
) {
@@ -1508,7 +1457,7 @@ impl AgentDiff {
return;
};
let action_log = thread.action_log(cx);
let action_log = thread.read(cx).action_log();
let changed_buffers = action_log.read(cx).changed_buffers(cx);
let mut unaffected = self.reviewing_editors.clone();
@@ -1627,7 +1576,7 @@ impl AgentDiff {
fn keep_all(
editor: &Entity<Editor>,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1647,7 +1596,7 @@ impl AgentDiff {
fn reject_all(
editor: &Entity<Editor>,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1667,7 +1616,7 @@ impl AgentDiff {
fn keep(
editor: &Entity<Editor>,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1680,7 +1629,7 @@ impl AgentDiff {
fn reject(
editor: &Entity<Editor>,
thread: &AgentDiffThread,
thread: &Entity<AcpThread>,
window: &mut Window,
cx: &mut App,
) -> PostReviewState {
@@ -1703,7 +1652,7 @@ impl AgentDiff {
fn review_in_active_editor(
&mut self,
workspace: &mut Workspace,
review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Task<Result<()>>> {
@@ -1725,7 +1674,7 @@ impl AgentDiff {
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
&& let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
{
let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
let mut keys = changed_buffers.keys().cycle();
keys.find(|k| *k == &curr_buffer);
@@ -1768,12 +1717,11 @@ mod tests {
use super::*;
use crate::Keep;
use acp_thread::AgentConnection as _;
use agent_settings::AgentSettings;
use editor::EditorSettings;
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use settings::{Settings, SettingsStore};
use settings::SettingsStore;
use std::{path::Path, rc::Rc};
use util::path;
@@ -1782,13 +1730,8 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
});
@@ -1815,8 +1758,7 @@ mod tests {
.await
.unwrap();
let thread = AgentDiffThread::AcpThread(thread);
let action_log = cx.read(|cx| thread.action_log(cx));
let action_log = cx.read(|cx| thread.read(cx).action_log().clone());
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
@@ -1942,13 +1884,8 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
prompt_store::init(cx);
workspace::init_settings(cx);
theme::init(theme::LoadThemes::JustBase, cx);
EditorSettings::register(cx);
language_model::init_settings(cx);
workspace::register_project_item::<Editor>(cx);
});
@@ -2004,7 +1941,6 @@ mod tests {
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
// Set the active thread
let thread = AgentDiffThread::AcpThread(thread);
cx.update(|window, cx| {
AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
});

View File

@@ -47,6 +47,7 @@ impl AgentModelSelector {
}
}
},
true, // Use popover styles for picker
window,
cx,
)

View File

@@ -16,10 +16,9 @@ use serde::{Deserialize, Serialize};
use settings::{
DefaultAgentView as DefaultView, LanguageModelProviderSetting, LanguageModelSelection,
};
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
@@ -39,6 +38,10 @@ use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
};
use crate::{ManageProfiles, context_store::ContextStore};
use crate::{
inline_assistant::ContextProviders,
ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal},
};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow};
@@ -455,6 +458,10 @@ pub struct AgentPanel {
}
impl AgentPanel {
pub(crate) fn workspace(&self) -> WeakEntity<Workspace> {
self.workspace.clone()
}
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
let selected_agent = self.selected_agent.clone();
@@ -1892,6 +1899,9 @@ impl AgentPanel {
.anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone())
.menu({
let selected_agent = self.selected_agent.clone();
let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
let workspace = self.workspace.clone();
let is_via_collab = workspace
.update(cx, |workspace, cx| {
@@ -1905,7 +1915,6 @@ impl AgentPanel {
let active_thread = active_thread.clone();
Some(ContextMenu::build(window, cx, |menu, _window, cx| {
menu.context(focus_handle.clone())
.header("Zed Agent")
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
@@ -1929,9 +1938,11 @@ impl AgentPanel {
}
})
.item(
ContextMenuEntry::new("New Thread")
.action(NewThread.boxed_clone())
.icon(IconName::Thread)
ContextMenuEntry::new("Zed Agent")
.when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::ZedAgent)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
@@ -1955,10 +1966,10 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Text Thread")
ContextMenuEntry::new("Text Thread")
.action(NewTextThread.boxed_clone())
.icon(IconName::TextThread)
.icon_color(Color::Muted)
.action(NewTextThread.boxed_clone())
.handler({
let workspace = workspace.clone();
move |window, cx| {
@@ -1983,7 +1994,10 @@ impl AgentPanel {
.separator()
.header("External Agents")
.item(
ContextMenuEntry::new("New Claude Code")
ContextMenuEntry::new("Claude Code")
.when(is_agent_selected(AgentType::ClaudeCode), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2009,7 +2023,10 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Codex CLI")
ContextMenuEntry::new("Codex CLI")
.when(is_agent_selected(AgentType::Codex), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiOpenAi)
.disabled(is_via_collab)
.icon_color(Color::Muted)
@@ -2035,7 +2052,10 @@ impl AgentPanel {
}),
)
.item(
ContextMenuEntry::new("New Gemini CLI")
ContextMenuEntry::new("Gemini CLI")
.when(is_agent_selected(AgentType::Gemini), |this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
})
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.disabled(is_via_collab)
@@ -2061,8 +2081,8 @@ impl AgentPanel {
}),
)
.map(|mut menu| {
let agent_server_store_read = agent_server_store.read(cx);
let agent_names = agent_server_store_read
let agent_server_store = agent_server_store.read(cx);
let agent_names = agent_server_store
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME
@@ -2071,21 +2091,38 @@ impl AgentPanel {
})
.cloned()
.collect::<Vec<_>>();
let custom_settings = cx
.global::<SettingsStore>()
.get::<AllAgentServersSettings>(None)
.custom
.clone();
for agent_name in agent_names {
let icon_path = agent_server_store_read.agent_icon(&agent_name);
let mut entry =
ContextMenuEntry::new(format!("New {}", agent_name));
let icon_path = agent_server_store.agent_icon(&agent_name);
let mut entry = ContextMenuEntry::new(agent_name.clone());
let command = custom_settings
.get(&agent_name.0)
.map(|settings| settings.command.clone())
.unwrap_or(placeholder_command());
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_svg(icon_path);
} else {
entry = entry.icon(IconName::Terminal);
}
entry = entry
.when(
is_agent_selected(AgentType::Custom {
name: agent_name.0.clone(),
command: command.clone(),
}),
|this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
},
)
.icon_color(Color::Muted)
.disabled(is_via_collab)
.handler({
@@ -2125,18 +2162,27 @@ impl AgentPanel {
}
}
});
menu = menu.item(entry);
}
menu
})
.separator()
.link(
"Add Other Agents",
OpenBrowser {
url: zed_urls::external_agents_docs(cx),
}
.boxed_clone(),
.item(
ContextMenuEntry::new("Add More Agents")
.icon(IconName::Plus)
.icon_color(Color::Muted)
.handler({
move |window, cx| {
window.dispatch_action(Box::new(zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::AgentServers,
),
id: None,
}), cx)
}
}),
)
}))
}
@@ -2149,7 +2195,7 @@ impl AgentPanel {
.id("selected_agent_icon")
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px(DynamicSpacing::Base02.rems(cx))
this.px_1()
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
@@ -2158,7 +2204,7 @@ impl AgentPanel {
.when(!has_custom_icon, |this| {
this.when_some(self.selected_agent.icon(), |this, icon| {
let label = selected_agent_label.clone();
this.px(DynamicSpacing::Base02.rems(cx))
this.px_1()
.child(Icon::new(icon).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
@@ -2661,17 +2707,12 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
else {
return;
};
let prompt_store = None;
let thread_store = None;
let context_store = cx.new(|_| ContextStore::new(project.clone()));
let context_providers = ContextProviders::empty(project, cx);
assistant.assist(
prompt_editor,
self.workspace.clone(),
context_store,
project,
prompt_store,
thread_store,
initial_prompt,
context_providers,
self.workspace.clone(),
window,
cx,
)

View File

@@ -9,10 +9,11 @@ mod context_picker;
mod context_server_configuration;
mod context_store;
mod context_strip;
#[cfg(test)]
mod evals;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
mod message_editor;
mod profile_selector;
mod slash_command;
mod slash_command_picker;
@@ -31,7 +32,10 @@ use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{Action, App, Entity, SharedString, actions};
use language::LanguageRegistry;
use language::{
LanguageRegistry,
language_settings::{AllLanguageSettings, EditPredictionProvider},
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
@@ -248,8 +252,6 @@ pub fn init(
is_eval: bool,
cx: &mut App,
) {
AgentSettings::register(cx);
assistant_text_thread::init(client.clone(), cx);
rules_library::init(cx);
if !is_eval {
@@ -289,7 +291,25 @@ pub fn init(
fn update_command_palette_filter(cx: &mut App) {
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
let agent_enabled = AgentSettings::get_global(cx).enabled;
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
.edit_predictions
.provider;
CommandPaletteFilter::update_global(cx, |filter, _| {
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
if disable_ai {
filter.hide_namespace("agent");
filter.hide_namespace("assistant");
@@ -298,42 +318,47 @@ fn update_command_palette_filter(cx: &mut App) {
filter.hide_namespace("zed_predict_onboarding");
filter.hide_namespace("edit_prediction");
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
filter.hide_action_types(&edit_prediction_actions);
filter.hide_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
} else {
filter.show_namespace("agent");
if agent_enabled {
filter.show_namespace("agent");
} else {
filter.hide_namespace("agent");
}
filter.show_namespace("assistant");
filter.show_namespace("copilot");
match edit_prediction_provider {
EditPredictionProvider::None => {
filter.hide_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.hide_action_types(&edit_prediction_actions);
}
EditPredictionProvider::Copilot => {
filter.show_namespace("edit_prediction");
filter.show_namespace("copilot");
filter.hide_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
EditPredictionProvider::Supermaven => {
filter.show_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.show_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
EditPredictionProvider::Zed
| EditPredictionProvider::Codestral
| EditPredictionProvider::Experimental(_) => {
filter.show_namespace("edit_prediction");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.show_action_types(edit_prediction_actions.iter());
}
}
filter.show_namespace("zed_predict_onboarding");
filter.show_namespace("edit_prediction");
use editor::actions::{
AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction,
PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction,
};
let edit_prediction_actions = [
TypeId::of::<AcceptEditPrediction>(),
TypeId::of::<AcceptPartialEditPrediction>(),
TypeId::of::<ShowEditPrediction>(),
TypeId::of::<NextEditPrediction>(),
TypeId::of::<PreviousEditPrediction>(),
TypeId::of::<ToggleEditPrediction>(),
];
filter.show_action_types(edit_prediction_actions.iter());
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
}
});
@@ -423,3 +448,137 @@ fn register_slash_commands(cx: &mut App) {
})
.detach();
}
#[cfg(test)]
mod tests {
use super::*;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode};
use command_palette_hooks::CommandPaletteFilter;
use editor::actions::AcceptEditPrediction;
use gpui::{BorrowAppContext, TestAppContext, px};
use project::DisableAiSettings;
use settings::{
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
};
#[gpui::test]
fn test_agent_command_palette_visibility(cx: &mut TestAppContext) {
// Init settings
cx.update(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
command_palette_hooks::init(cx);
AgentSettings::register(cx);
DisableAiSettings::register(cx);
AllLanguageSettings::register(cx);
});
let agent_settings = AgentSettings {
enabled: true,
button: true,
dock: DockPosition::Right,
default_width: px(300.),
default_height: px(600.),
default_model: None,
inline_assistant_model: None,
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: vec![],
default_profile: AgentProfileId::default(),
default_view: DefaultAgentView::Thread,
profiles: Default::default(),
always_allow_tool_actions: false,
notify_when_agent_waiting: NotifyWhenAgentWaiting::default(),
play_sound_when_agent_done: false,
single_file_review: false,
model_parameters: vec![],
preferred_completion_mode: CompletionMode::Normal,
enable_feedback: false,
expand_edit_card: true,
expand_terminal_card: true,
use_modifier_to_send: true,
message_editor_min_lines: 1,
};
cx.update(|cx| {
AgentSettings::override_global(agent_settings.clone(), cx);
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
// Initial update
update_command_palette_filter(cx);
});
// Assert visible
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
!filter.is_hidden(&NewThread),
"NewThread should be visible by default"
);
});
// Disable agent
cx.update(|cx| {
let mut new_settings = agent_settings.clone();
new_settings.enabled = false;
AgentSettings::override_global(new_settings, cx);
// Trigger update
update_command_palette_filter(cx);
});
// Assert hidden
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
filter.is_hidden(&NewThread),
"NewThread should be hidden when agent is disabled"
);
});
// Test EditPredictionProvider
// Enable EditPredictionProvider::Copilot
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project
.all_languages
.features
.get_or_insert(Default::default())
.edit_prediction_provider = Some(EditPredictionProvider::Copilot);
});
});
update_command_palette_filter(cx);
});
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
!filter.is_hidden(&AcceptEditPrediction),
"EditPrediction should be visible when provider is Copilot"
);
});
// Disable EditPredictionProvider (None)
cx.update(|cx| {
cx.update_global::<SettingsStore, _>(|store, cx| {
store.update_user_settings(cx, |s| {
s.project
.all_languages
.features
.get_or_insert(Default::default())
.edit_prediction_provider = Some(EditPredictionProvider::None);
});
});
update_command_palette_filter(cx);
});
cx.update(|cx| {
let filter = CommandPaletteFilter::try_global(cx).unwrap();
assert!(
filter.is_hidden(&AcceptEditPrediction),
"EditPrediction should be hidden when provider is None"
);
});
}
}

View File

@@ -1082,10 +1082,7 @@ mod tests {
};
use gpui::TestAppContext;
use indoc::indoc;
use language::{
Buffer, Language, LanguageConfig, LanguageMatcher, Point, language_settings,
tree_sitter_rust,
};
use language::{Buffer, Language, LanguageConfig, LanguageMatcher, Point, tree_sitter_rust};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
use settings::SettingsStore;
@@ -1465,8 +1462,6 @@ mod tests {
fn init_test(cx: &mut TestAppContext) {
cx.update(LanguageModelRegistry::test);
cx.set_global(cx.update(SettingsStore::test));
cx.update(Project::init_settings);
cx.update(language_settings::init);
}
fn simulate_response_stream(

View File

@@ -1075,8 +1075,6 @@ mod tests {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
@@ -1091,7 +1089,7 @@ mod tests {
}
#[gpui::test]
async fn test_large_file_uses_outline(cx: &mut TestAppContext) {
async fn test_large_file_uses_fallback(cx: &mut TestAppContext) {
init_test_settings(cx);
// Create a large file that exceeds AUTO_OUTLINE_SIZE
@@ -1103,16 +1101,16 @@ mod tests {
let file_context = load_context_for("file.txt", large_content, cx).await;
// Should contain some of the actual file content
assert!(
file_context
.text
.contains(&format!("# File outline for {}", path!("test/file.txt"))),
"Large files should not get an outline"
file_context.text.contains(LINE),
"Should contain some of the file content"
);
// Should be much smaller than original
assert!(
file_context.text.len() < content_len,
"Outline should be smaller than original content"
file_context.text.len() < content_len / 10,
"Should be significantly smaller than original content"
);
}

View File

@@ -38,6 +38,7 @@ use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::inline_assistant::ContextProviders;
use crate::{context::RULES_ICON, context_store::ContextStore};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -172,12 +173,18 @@ pub(super) struct ContextPicker {
impl ContextPicker {
pub fn new(
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
context_store: WeakEntity<ContextStore>,
context_stores: ContextProviders,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let context_store = context_stores.context_store.downgrade();
let thread_store = context_stores.thread_store.clone();
let prompt_store = context_stores
.prompt_store
.as_ref()
.map(Entity::downgrade)
.clone();
let subscriptions = context_store
.upgrade()
.map(|context_store| {

View File

@@ -27,6 +27,7 @@ use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::inline_assistant::ContextProviders;
use crate::{
context::{AgentContextHandle, AgentContextKey, RULES_ICON},
context_store::ContextStore,
@@ -42,7 +43,7 @@ use super::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
};
use crate::message_editor::ContextCreasesAddon;
use crate::inline_prompt_editor::ContextCreasesAddon;
pub(crate) enum Match {
File(FileMatch),
@@ -245,17 +246,15 @@ pub struct ContextPickerCompletionProvider {
impl ContextPickerCompletionProvider {
pub fn new(
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
context_stores: ContextProviders,
editor: WeakEntity<Editor>,
exclude_buffer: Option<WeakEntity<Buffer>>,
) -> Self {
Self {
workspace,
context_store,
thread_store,
prompt_store,
context_store: context_stores.context_store.downgrade(),
thread_store: context_stores.thread_store,
prompt_store: context_stores.prompt_store.as_ref().map(Entity::downgrade),
editor,
excluded_buffer: exclude_buffer,
}
@@ -278,6 +277,8 @@ impl ContextPickerCompletionProvider {
icon_path: Some(mode.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -386,6 +387,8 @@ impl ContextPickerCompletionProvider {
icon_path: Some(action.icon().path().into()),
documentation: None,
source: project::CompletionSource::Custom,
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
// This ensures that when a user accepts this completion, the
// completion menu will still be shown after "@category " is
@@ -417,6 +420,8 @@ impl ContextPickerCompletionProvider {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(thread_entry.title().to_string(), None),
match_start: None,
snippet_deduplication_key: None,
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
@@ -484,6 +489,8 @@ impl ContextPickerCompletionProvider {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(rules.title.to_string(), None),
match_start: None,
snippet_deduplication_key: None,
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
@@ -524,6 +531,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::ToolWeb.path().into()),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::ToolWeb.path().into(),
@@ -612,6 +621,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(completion_icon_path),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
crease_icon_path,
@@ -689,6 +700,8 @@ impl ContextPickerCompletionProvider {
documentation: None,
source: project::CompletionSource::Custom,
icon_path: Some(IconName::Code.path().into()),
match_start: None,
snippet_deduplication_key: None,
insert_text_mode: None,
confirm: Some(confirm_completion_callback(
IconName::Code.path().into(),
@@ -1182,10 +1195,8 @@ mod tests {
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);
});
app_state
@@ -1280,7 +1291,7 @@ mod tests {
editor
});
let context_store = cx.new(|_| ContextStore::new(project.downgrade()));
let context_providers = ContextProviders::empty(project.downgrade(), cx);
let editor_entity = editor.downgrade();
editor.update_in(&mut cx, |editor, window, cx| {
@@ -1295,11 +1306,10 @@ mod tests {
.map(Entity::downgrade)
});
window.focus(&editor.focus_handle(cx));
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.downgrade(),
context_store.downgrade(),
None,
None,
context_providers,
editor_entity,
last_opened_buffer,
))));
@@ -1486,10 +1496,8 @@ mod tests {
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);
});
app_state
@@ -1686,11 +1694,6 @@ mod tests {
let store = SettingsStore::test(cx);
cx.set_global(store);
theme::init(theme::LoadThemes::JustBase, cx);
client::init_settings(cx);
language::init(cx);
Project::init_settings(cx);
workspace::init_settings(cx);
editor::init_settings(cx);
});
}
}

View File

@@ -39,6 +39,10 @@ pub enum ContextStoreEvent {
impl EventEmitter<ContextStoreEvent> for ContextStore {}
impl ContextStore {
pub fn project(&self) -> WeakEntity<Project> {
self.project.clone()
}
pub fn new(project: WeakEntity<Project>) -> Self {
Self {
project,

View File

@@ -2,13 +2,13 @@ use crate::{
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
context_picker::ContextPicker,
inline_assistant::ContextProviders,
ui::{AddedContext, ContextPill},
};
use crate::{
context::AgentContextHandle,
context_store::{ContextStore, SuggestedContext},
};
use agent::HistoryStore;
use collections::HashSet;
use editor::Editor;
use gpui::{
@@ -42,10 +42,8 @@ pub struct ContextStrip {
impl ContextStrip {
pub fn new(
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
inline_services: ContextProviders,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
model_usage_context: ModelUsageContext,

1869
crates/agent_ui/src/evals.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::EditorSnapshot;
use editor::RowExt;
use editor::SelectionEffects;
use editor::scroll::ScrollOffset;
@@ -87,6 +88,38 @@ enum InlineAssistTarget {
Terminal(Entity<TerminalView>),
}
#[derive(Clone)]
pub(crate) struct ContextProviders {
pub(crate) workspace: Option<WeakEntity<Workspace>>,
pub(crate) prompt_store: Option<Entity<PromptStore>>,
pub(crate) thread_store: Option<WeakEntity<HistoryStore>>,
pub(crate) context_store: Entity<ContextStore>,
}
impl ContextProviders {
pub(crate) fn project(&self, cx: &mut App) -> WeakEntity<Project> {
self.context_store.read(cx).project()
}
pub(crate) fn from_panel(agent_panel: &AgentPanel) -> Self {
Self {
workspace: Some(agent_panel.workspace()),
prompt_store: agent_panel.prompt_store().as_ref().cloned(),
thread_store: Some(agent_panel.thread_store().downgrade()),
context_store: agent_panel.inline_assist_context_store().clone(),
}
}
pub(crate) fn empty(project: WeakEntity<Project>, cx: &mut App) -> Self {
Self {
workspace: None,
prompt_store: None,
thread_store: None,
context_store: cx.new(|_| ContextStore::new(project.clone())),
}
}
}
pub struct InlineAssistant {
next_assist_id: InlineAssistId,
next_assist_group_id: InlineAssistGroupId,
@@ -274,11 +307,8 @@ impl InlineAssistant {
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
return;
};
let agent_panel = agent_panel.read(cx);
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
let thread_store = Some(agent_panel.thread_store().downgrade());
let context_store = agent_panel.inline_assist_context_store().clone();
let services = ContextProviders::from_panel(agent_panel.read(cx));
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -287,10 +317,6 @@ impl InlineAssistant {
assistant.assist(
&active_editor,
cx.entity().downgrade(),
context_store,
workspace.project().downgrade(),
prompt_store,
thread_store,
action.prompt.clone(),
window,
cx,
@@ -302,10 +328,8 @@ impl InlineAssistant {
assistant.assist(
&active_terminal,
cx.entity().downgrade(),
workspace.project().downgrade(),
prompt_store,
thread_store,
action.prompt.clone(),
services,
window,
cx,
)
@@ -350,25 +374,20 @@ impl InlineAssistant {
}
}
pub fn assist(
fn codegen_ranges(
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>,
snapshot: &EditorSnapshot,
window: &mut Window,
cx: &mut App,
) {
let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let selections = editor.selections.all::<Point>(&snapshot.display_snapshot);
let newest_selection = editor
.selections
.newest::<Point>(&snapshot.display_snapshot);
(snapshot, selections, newest_selection)
) -> Option<(Vec<Range<Anchor>>, Selection<Point>)> {
let (initial_selections, newest_selection) = editor.update(cx, |editor, _| {
(
editor.selections.all::<Point>(&snapshot.display_snapshot),
editor
.selections
.newest::<Point>(&snapshot.display_snapshot),
)
});
// Check if there is already an inline assistant that contains the
@@ -381,7 +400,7 @@ impl InlineAssistant {
&& newest_selection.end.row <= range.end.row
{
self.focus_assist(*assist_id, window, cx);
return;
return None;
}
}
}
@@ -473,6 +492,25 @@ impl InlineAssistant {
}
}
Some((codegen_ranges, newest_selection))
}
pub fn assist(
&mut self,
editor: &Entity<Editor>,
initial_prompt: Option<String>,
services: ContextProviders,
window: &mut Window,
cx: &mut App,
) {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((codegen_ranges, newest_selection)) =
self.codegen_ranges(editor, &snapshot, window, cx)
else {
return;
};
let assist_group_id = self.next_assist_group_id.post_inc();
let prompt_buffer = cx.new(|cx| {
MultiBuffer::singleton(
@@ -484,53 +522,26 @@ impl InlineAssistant {
let mut assists = Vec::new();
let mut assist_to_focus = None;
for range in codegen_ranges {
let assist_id = self.next_assist_id.post_inc();
let codegen = cx.new(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
None,
context_store.clone(),
project.clone(),
prompt_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
)
});
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
editor_margins,
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store.as_ref().map(|s| s.downgrade()),
window,
cx,
)
});
let (assist_id, prompt_block_id, end_block_id, prompt_editor) = self.single_assist(
range.clone(),
editor,
prompt_buffer.clone(),
services.clone(),
window,
cx,
);
if assist_to_focus.is_none() {
let focus_assist = if newest_selection.reversed {
range.start.to_point(snapshot) == newest_selection.start
range.start.to_point(&snapshot) == newest_selection.start
} else {
range.end.to_point(snapshot) == newest_selection.end
range.end.to_point(&snapshot) == newest_selection.end
};
if focus_assist {
assist_to_focus = Some(assist_id);
}
}
let [prompt_block_id, end_block_id] =
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
assists.push((
assist_id,
range,
@@ -574,11 +585,66 @@ impl InlineAssistant {
}
}
fn single_assist(
&mut self,
range: Range<Anchor>,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
prompt_buffer: Entity<MultiBuffer>,
services: ContextProviders,
project: &WeakEntity<Project>,
window: &mut Window,
cx: &mut App,
) -> (
InlineAssistId,
CustomBlockId,
CustomBlockId,
Entity<PromptEditor<BufferCodegen>>,
) {
let assist_id = self.next_assist_id.post_inc();
let codegen = cx.new(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
None,
context_store.clone(),
project.clone(),
prompt_store.clone(),
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
)
});
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
editor_margins,
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store.as_ref().map(|s| s.downgrade()),
window,
cx,
)
});
let [prompt_block_id, end_block_id] =
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
(assist_id, prompt_block_id, end_block_id, prompt_editor)
}
pub fn suggest_assist(
&mut self,
editor: &Entity<Editor>,
mut range: Range<Anchor>,
initial_prompt: String,
prompt_buffer: Entity<MultiBuffer>,
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
@@ -587,9 +653,8 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) -> InlineAssistId {
/// !!!!!!!!
let assist_group_id = self.next_assist_group_id.post_inc();
let prompt_buffer = cx.new(|cx| Buffer::local(&initial_prompt, cx));
let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx));
let assist_id = self.next_assist_id.post_inc();
@@ -1879,11 +1944,15 @@ impl CodeActionProvider for AssistantCodeActionProvider {
.context("invalid range")?;
let prompt_store = prompt_store.await.ok();
const PROMPT: &'static str = "Fix Diagnostics";
cx.update_global(|assistant: &mut InlineAssistant, window, cx| {
let prompt_buffer = cx.new(|cx| Buffer::local(PROMPT, cx));
let prompt_buffer = cx.new(|cx| MultiBuffer::singleton(prompt_buffer, cx));
let assist_id = assistant.suggest_assist(
&editor,
range,
"Fix Diagnostics".into(),
prompt_buffer,
None,
true,
workspace,

View File

@@ -1,8 +1,8 @@
use crate::context_store::ContextStore;
use agent::HistoryStore;
use collections::VecDeque;
use collections::{HashMap, VecDeque};
use editor::actions::Paste;
use editor::display_map::EditorMargins;
use editor::display_map::{CreaseId, EditorMargins};
use editor::{Addon, AnchorRangeExt as _};
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp},
@@ -17,6 +17,7 @@ use parking_lot::Mutex;
use prompt_store::PromptStore;
use settings::Settings;
use std::cmp;
use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use theme::ThemeSettings;
@@ -27,12 +28,16 @@ use zed_actions::agent::ToggleModelSelector;
use crate::agent_model_selector::AgentModelSelector;
use crate::buffer_codegen::BufferCodegen;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
use crate::context::{AgentContextHandle, AgentContextKey};
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
use crate::context_store::{ContextStore, ContextStoreEvent};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
use crate::inline_assistant::ContextProviders;
use crate::terminal_codegen::TerminalCodegen;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
use crate::{RemoveAllContext, ToggleContextPicker};
use crate::{
CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
ToggleContextPicker,
};
pub struct PromptEditor<T> {
pub editor: Entity<Editor>,
@@ -769,10 +774,8 @@ impl PromptEditor<BufferCodegen> {
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
fs: Arc<dyn Fs>,
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
services: ContextProviders,
window: &mut Window,
cx: &mut Context<PromptEditor<BufferCodegen>>,
) -> PromptEditor<BufferCodegen> {
@@ -941,10 +944,8 @@ impl PromptEditor<TerminalCodegen> {
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<TerminalCodegen>,
fs: Arc<dyn Fs>,
context_store: Entity<ContextStore>,
context_providers: ContextProviders,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -1157,3 +1158,156 @@ impl GenerationMode {
}
}
}
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
pub struct MessageCrease {
pub range: Range<usize>,
pub icon_path: SharedString,
pub label: SharedString,
/// None for a deserialized message, Some otherwise.
pub context: Option<AgentContextHandle>,
}
#[derive(Default)]
pub struct ContextCreasesAddon {
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
_subscription: Option<Subscription>,
}
impl Addon for ContextCreasesAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
impl ContextCreasesAddon {
pub fn new() -> Self {
Self {
creases: HashMap::default(),
_subscription: None,
}
}
pub fn add_creases(
&mut self,
context_store: &Entity<ContextStore>,
key: AgentContextKey,
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
cx: &mut Context<Editor>,
) {
self.creases.entry(key).or_default().extend(creases);
self._subscription = Some(
cx.subscribe(context_store, |editor, _, event, cx| match event {
ContextStoreEvent::ContextRemoved(key) => {
let Some(this) = editor.addon_mut::<Self>() else {
return;
};
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
.creases
.remove(key)
.unwrap_or_default()
.into_iter()
.unzip();
let ranges = editor
.remove_creases(crease_ids, cx)
.into_iter()
.map(|(_, range)| range)
.collect::<Vec<_>>();
editor.unfold_ranges(&ranges, false, false, cx);
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
cx.notify();
}
}),
)
}
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
self.creases
}
}
pub fn extract_message_creases(
editor: &mut Editor,
cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let mut contexts_by_crease_id = editor
.addon_mut::<ContextCreasesAddon>()
.map(std::mem::take)
.unwrap_or_default()
.into_inner()
.into_iter()
.flat_map(|(key, creases)| {
let context = key.0;
creases
.into_iter()
.map(move |(id, _)| (id, context.clone()))
})
.collect::<HashMap<_, _>>();
// Filter the addon's list of creases based on what the editor reports,
// since the addon might have removed creases in it.
editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.crease_snapshot
.creases()
.filter_map(|(id, crease)| {
Some((
id,
(
crease.range().to_offset(&buffer_snapshot),
crease.metadata()?.clone(),
),
))
})
.map(|(id, (range, metadata))| {
let context = contexts_by_crease_id.remove(&id);
MessageCrease {
range,
context,
label: metadata.label,
icon_path: metadata.icon_path,
}
})
.collect()
})
}
pub fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
context_store: &Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
.map(|crease| {
let start = buffer_snapshot.anchor_after(crease.range.start);
let end = buffer_snapshot.anchor_before(crease.range.end);
crease_for_mention(
crease.label.clone(),
crease.icon_path.clone(),
start..end,
cx.weak_entity(),
)
})
.collect::<Vec<_>>();
let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
for (crease, id) in message_creases.iter().zip(ids) {
if let Some(context) = crease.context.as_ref() {
let key = AgentContextKey(context.clone());
addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::{cmp::Reverse, sync::Arc};
use collections::{HashSet, IndexMap};
use collections::IndexMap;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use language_model::{
@@ -19,14 +19,26 @@ pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
window: &mut Window,
cx: &mut Context<LanguageModelSelector>,
) -> LanguageModelSelector {
let delegate = LanguageModelPickerDelegate::new(get_active_model, on_model_changed, window, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
let delegate = LanguageModelPickerDelegate::new(
get_active_model,
on_model_changed,
popover_styles,
window,
cx,
);
if popover_styles {
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
} else {
Picker::list(delegate, window, cx).show_scrollbar(true)
}
}
fn all_models(cx: &App) -> GroupedModels {
@@ -45,7 +57,7 @@ fn all_models(cx: &App) -> GroupedModels {
})
.collect();
let other = providers
let all = providers
.iter()
.flat_map(|provider| {
provider
@@ -58,7 +70,7 @@ fn all_models(cx: &App) -> GroupedModels {
})
.collect();
GroupedModels::new(other, recommended)
GroupedModels::new(all, recommended)
}
#[derive(Clone)]
@@ -75,12 +87,14 @@ pub struct LanguageModelPickerDelegate {
selected_index: usize,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>,
popover_styles: bool,
}
impl LanguageModelPickerDelegate {
fn new(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Self {
@@ -113,6 +127,7 @@ impl LanguageModelPickerDelegate {
}
},
)],
popover_styles,
}
}
@@ -177,7 +192,7 @@ impl LanguageModelPickerDelegate {
}
_ => {
log::error!(
"Failed to authenticate provider: {}: {err}",
"Failed to authenticate provider: {}: {err:#}",
provider_name.0
);
}
@@ -195,33 +210,24 @@ impl LanguageModelPickerDelegate {
struct GroupedModels {
recommended: Vec<ModelInfo>,
other: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
all: IndexMap<LanguageModelProviderId, Vec<ModelInfo>>,
}
impl GroupedModels {
pub fn new(other: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let recommended_ids = recommended
.iter()
.map(|info| (info.model.provider_id(), info.model.id()))
.collect::<HashSet<_>>();
let mut other_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in other {
if recommended_ids.contains(&(model.model.provider_id(), model.model.id())) {
continue;
}
pub fn new(all: Vec<ModelInfo>, recommended: Vec<ModelInfo>) -> Self {
let mut all_by_provider: IndexMap<_, Vec<ModelInfo>> = IndexMap::default();
for model in all {
let provider = model.model.provider_id();
if let Some(models) = other_by_provider.get_mut(&provider) {
if let Some(models) = all_by_provider.get_mut(&provider) {
models.push(model);
} else {
other_by_provider.insert(provider, vec![model]);
all_by_provider.insert(provider, vec![model]);
}
}
Self {
recommended,
other: other_by_provider,
all: all_by_provider,
}
}
@@ -237,7 +243,7 @@ impl GroupedModels {
);
}
for models in self.other.values() {
for models in self.all.values() {
if models.is_empty() {
continue;
}
@@ -252,20 +258,6 @@ impl GroupedModels {
}
entries
}
fn model_infos(&self) -> Vec<ModelInfo> {
let other = self
.other
.values()
.flat_map(|model| model.iter())
.cloned()
.collect::<Vec<_>>();
self.recommended
.iter()
.chain(&other)
.cloned()
.collect::<Vec<_>>()
}
}
enum LanguageModelPickerEntry {
@@ -410,8 +402,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.collect::<Vec<_>>();
let available_models = all_models
.model_infos()
.iter()
.all
.values()
.flat_map(|models| models.iter())
.filter(|m| configured_provider_ids.contains(&m.model.provider_id()))
.cloned()
.collect::<Vec<_>>();
@@ -499,17 +492,15 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.start_slot(
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.child(
h_flex()
.w_full()
.pl_0p5()
.gap_1p5()
.w(px(240.))
.child(
Icon::new(model_info.icon)
.color(model_icon_color)
.size(IconSize::Small),
)
.child(Label::new(model_info.model.name().0).truncate()),
)
.end_slot(div().pr_3().when(is_selected, |this| {
@@ -530,6 +521,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
if !self.popover_styles {
return None;
}
Some(
h_flex()
.w_full()
@@ -745,46 +740,52 @@ mod tests {
}
#[gpui::test]
fn test_exclude_recommended_models(_cx: &mut TestAppContext) {
fn test_recommended_models_also_appear_in_other(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![
("zed", "claude"), // Should be filtered out from "other"
("zed", "claude"), // Should also appear in "other"
("zed", "gemini"),
("copilot", "o3"),
]);
let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_other_models = grouped_models
.other
let actual_all_models = grouped_models
.all
.values()
.flatten()
.cloned()
.collect::<Vec<_>>();
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/o3"]);
// Recommended models should also appear in "all"
assert_models_eq(
actual_all_models,
vec!["zed/claude", "zed/gemini", "copilot/o3"],
);
}
#[gpui::test]
fn test_dont_exclude_models_from_other_providers(_cx: &mut TestAppContext) {
fn test_models_from_different_providers(_cx: &mut TestAppContext) {
let recommended_models = create_models(vec![("zed", "claude")]);
let all_models = create_models(vec![
("zed", "claude"), // Should be filtered out from "other"
("zed", "claude"), // Should also appear in "other"
("zed", "gemini"),
("copilot", "claude"), // Should not be filtered out from "other"
("copilot", "claude"), // Different provider, should appear in "other"
]);
let grouped_models = GroupedModels::new(all_models, recommended_models);
let actual_other_models = grouped_models
.other
let actual_all_models = grouped_models
.all
.values()
.flatten()
.cloned()
.collect::<Vec<_>>();
// Recommended models should not appear in "other"
assert_models_eq(actual_other_models, vec!["zed/gemini", "copilot/claude"]);
// All models should appear in "all" regardless of recommended status
assert_models_eq(
actual_all_models,
vec!["zed/claude", "zed/gemini", "copilot/claude"],
);
}
}

View File

@@ -1,166 +0,0 @@
use std::ops::Range;
use collections::HashMap;
use editor::display_map::CreaseId;
use editor::{Addon, AnchorRangeExt, Editor};
use gpui::{Entity, Subscription};
use ui::prelude::*;
use crate::{
context::{AgentContextHandle, AgentContextKey},
context_picker::crease_for_mention,
context_store::{ContextStore, ContextStoreEvent},
};
/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
#[derive(Clone, Debug)]
pub struct MessageCrease {
pub range: Range<usize>,
pub icon_path: SharedString,
pub label: SharedString,
/// None for a deserialized message, Some otherwise.
pub context: Option<AgentContextHandle>,
}
#[derive(Default)]
pub struct ContextCreasesAddon {
creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
_subscription: Option<Subscription>,
}
impl Addon for ContextCreasesAddon {
fn to_any(&self) -> &dyn std::any::Any {
self
}
fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
impl ContextCreasesAddon {
pub fn new() -> Self {
Self {
creases: HashMap::default(),
_subscription: None,
}
}
pub fn add_creases(
&mut self,
context_store: &Entity<ContextStore>,
key: AgentContextKey,
creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
cx: &mut Context<Editor>,
) {
self.creases.entry(key).or_default().extend(creases);
self._subscription = Some(
cx.subscribe(context_store, |editor, _, event, cx| match event {
ContextStoreEvent::ContextRemoved(key) => {
let Some(this) = editor.addon_mut::<Self>() else {
return;
};
let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
.creases
.remove(key)
.unwrap_or_default()
.into_iter()
.unzip();
let ranges = editor
.remove_creases(crease_ids, cx)
.into_iter()
.map(|(_, range)| range)
.collect::<Vec<_>>();
editor.unfold_ranges(&ranges, false, false, cx);
editor.edit(ranges.into_iter().zip(replacement_texts), cx);
cx.notify();
}
}),
)
}
pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
self.creases
}
}
pub fn extract_message_creases(
editor: &mut Editor,
cx: &mut Context<'_, Editor>,
) -> Vec<MessageCrease> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let mut contexts_by_crease_id = editor
.addon_mut::<ContextCreasesAddon>()
.map(std::mem::take)
.unwrap_or_default()
.into_inner()
.into_iter()
.flat_map(|(key, creases)| {
let context = key.0;
creases
.into_iter()
.map(move |(id, _)| (id, context.clone()))
})
.collect::<HashMap<_, _>>();
// Filter the addon's list of creases based on what the editor reports,
// since the addon might have removed creases in it.
editor.display_map.update(cx, |display_map, cx| {
display_map
.snapshot(cx)
.crease_snapshot
.creases()
.filter_map(|(id, crease)| {
Some((
id,
(
crease.range().to_offset(&buffer_snapshot),
crease.metadata()?.clone(),
),
))
})
.map(|(id, (range, metadata))| {
let context = contexts_by_crease_id.remove(&id);
MessageCrease {
range,
context,
label: metadata.label,
icon_path: metadata.icon_path,
}
})
.collect()
})
}
pub fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
context_store: &Entity<ContextStore>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
.map(|crease| {
let start = buffer_snapshot.anchor_after(crease.range.start);
let end = buffer_snapshot.anchor_before(crease.range.end);
crease_for_mention(
crease.label.clone(),
crease.icon_path.clone(),
start..end,
cx.weak_entity(),
)
})
.collect::<Vec<_>>();
let ids = editor.insert_creases(creases.clone(), cx);
editor.fold_creases(creases, false, window, cx);
if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
for (crease, id) in message_creases.iter().zip(ids) {
if let Some(context) = crease.context.as_ref() {
let key = AgentContextKey(context.clone());
addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
}
}
}
}

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