Compare commits

...

156 Commits

Author SHA1 Message Date
Agus Zubiaga
2acd48f439 Checkpoint: Refactoring blink manager 2025-11-25 17:24:27 -03:00
Agus Zubiaga
1f9d5ef684 Always display terminal cursor when blinking is disabled (#43487)
Fixes an issue where the terminal cursor wouldn't always be displayed in
the default `blink: "terminal_controlled"` mode unless the terminal
requested cursor blinking.

Release Notes:

- N/A
2025-11-25 19:49:16 +00:00
Peter Tripp
83f0a3fd13 Redact sensitive environment variables in LSP Logs: Server Info (#43480)
Follow-up to: 
- https://github.com/zed-industries/zed/pull/43436
- https://github.com/zed-industries/zed/pull/42831

The changes in #42831 resulted in a regression where environment
variables in the Server Info view were no longer redact. The changes in
#43436 were insufficient as I was still seeing sensitive values in
Nightly e6fe95b4f2 (which includes
#43436).

CC: @SomeoneToIgnore (Hi! 👋 Thanks for keeping this redaction
functionality alive)

Release Notes:

- N/A
2025-11-25 21:00:31 +02:00
Ben Kunkle
7ecbf8cf60 zeta2: Remove expected context from evals (#43430)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-25 13:44:04 -05:00
Max Brunsfeld
fb0fcd86fd Add missing update of last_prediction_refresh (#43483)
Fixes a regression introduced in
https://github.com/zed-industries/zed/pull/43284 where edit predictions
stopped being throttled at all 😬

Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-25 10:43:46 -08:00
Max Brunsfeld
36708c910a Separate experimental edit prediction jumps feature from the Sweep AI prediction provider (#43481)
Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-25 10:36:45 -08:00
Smit Barmase
388fda2292 editor: Fix package version completion partial accept and improve sorting (#43473)
Closes #41723

This PR fixes an issue with accepting partial semver completions by
including `.` in the completion query. This makes the editor treat the
entire version string as the query, instead of breaking segment at last
`.` .

This PR also adds a test for sorting semver completions. The actual
sorting fix is handled in the `package-version-server` by having it
provide `sort_text`. More:
https://github.com/zed-industries/package-version-server/pull/10

<img width="600" alt="image"
src="https://github.com/user-attachments/assets/7657912f-c6da-4e05-956b-1c044918304f"
/>

Release Notes:

- Fixed an issue where accepting a completion for a semver version in
package.json would append the suggestion to the existing text instead of
replacing it.
- Improved the sorting of semver completions in package.json so the
latest versions appear at the top.
2025-11-25 23:27:11 +05:30
Bennet Bo Fenner
94f9b85859 acp: Only pass enabled MCP servers to agent (#43467)
Release Notes:

- Fix an issue where ACP agents would start MCP servers that were
disabled in Zed
2025-11-25 18:29:45 +01:00
Anthony Eid
1c072017a4 git: Make the version_control.{deleted/added} colors more accessible (#43475)
The new colors are easier to tell apart for people that are colorblind

cc: @mattermill 

## One Dark
### Before
<img width="723" height="212" alt="Screenshot 2025-11-25 at 12 13 14 PM"
src="https://github.com/user-attachments/assets/cea67b08-5662-4afa-8119-dbfcef53ada7"
/>

### After
<img width="711" height="109" alt="Screenshot 2025-11-25 at 12 14 16 PM"
src="https://github.com/user-attachments/assets/a42d88ea-1a85-4f48-8f5e-b9bedf321c62"
/>

## One Light
### Before
<img width="724" height="219" alt="Screenshot 2025-11-25 at 12 15 13 PM"
src="https://github.com/user-attachments/assets/c0176b8c-12bf-451c-8a2c-a2efd15463d1"
/>
### After
<img width="723" height="209" alt="Screenshot 2025-11-25 at 12 15 45 PM"
src="https://github.com/user-attachments/assets/b8858a11-29e2-4309-b1a6-c734f89f6d5e"
/>

Release Notes:

- N/A
2025-11-25 17:29:26 +00:00
Richard Feldman
8a992703a7 Add Gemini 3 support to Copilot (#43096)
Closes #43024

Release Notes:

- Add support for Gemini 3 to Copilot
2025-11-25 12:15:55 -05:00
Joseph T. Lyons
2053fea0a7 Add collaboration redirects (#43471)
Redirect:

https://zed.dev/docs/collaboration ->
https://zed.dev/docs/collaboration/overview
https://zed.dev/docs/channels ->
https://zed.dev/docs/collaboration/channels

Release Notes:

- N/A
2025-11-25 11:28:33 -05:00
Jakub Konka
552bc02783 git: Bring back auto-commit suggestions (#43470)
This got accidentally regressed in
https://github.com/zed-industries/zed/pull/42149.

Release Notes:

- Fixed displaying auto-commit suggestions for single staged entries.
2025-11-25 16:26:44 +00:00
Lukas Wirth
fafe1afa61 multi_buffer: Remove redundant buffer id field (#43459)
It is easy for us to get the two fields out of sync causing weird
problems, there is no reason to have both here so.

Release Notes:

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

Co-authored by: Antonio Scandurra <antonio@zed.dev>
2025-11-25 17:13:16 +01:00
Bennet Bo Fenner
ab80ef1845 mcp: Fix source property showing up as undefined in settings (#43417)
Follow up to #39021.

<img width="576" height="141" alt="image"
src="https://github.com/user-attachments/assets/c89885a4-e664-4614-9bb0-86442dff34ee"
/>

- Add migration to remove `source` tag because `ContextServerSettings`
is now untagged
- Fix typos in context server modal
- PR seems to have removed the `test_action_namespaces` test, which I
brought back in this PR

Release Notes:

- Fixed an issue where the `source` property of MCP settings would show
up as unrecognised
2025-11-25 16:03:21 +00:00
Joseph T. Lyons
9cae39449a Restructure collaboration docs (#43464)
Overview
    - Channels
    - Private calls

---

Up next would be to 

- [ ] Update any zed.dev links to point to items in this structure
- [ ] Update content in these docs (would prefer to do that in a
separate PR from this one)

Release Notes:

- N/A
2025-11-25 10:53:47 -05:00
Jason Lee
f58de21068 miniprofiler_ui: Improve MiniProfiler to use uniform list (#43457)
Release Notes:

- N/A

---

- Apply uniform_list for timing list for performance.
- Add paddings for window.
- Add space to `ms`, before: `100ms` after `100 ms`.

## Before 

<img width="1392" height="860" alt="image"
src="https://github.com/user-attachments/assets/9706a96f-7093-4d4f-832f-306948a9b17b"
/>

## After 

<img width="1392" height="864" alt="image"
src="https://github.com/user-attachments/assets/38df1b71-15e7-4101-b0c9-ecdcdb7752d7"
/>
2025-11-25 16:08:49 +01:00
David Kleingeld
1cbb49864c document how to do flamecharts in an easy way (#43461)
Release Notes:

- N/A
2025-11-25 15:01:38 +00:00
Lukas Wirth
f8965317c3 multi_buffer: Fix up some anchor checks (#43454)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-25 13:41:19 +00:00
David Kleingeld
a359a5a1f2 Add performance doc (#43265)
Release Notes:

- N/A
2025-11-25 13:49:27 +01:00
Piotr Osiewicz
7651854bbd ci: Do not show output of failed tests at the end too (#43449)
This reverts #39643, effectively

For the record, @SomeoneToIgnore found it quite cumbersome to scroll
through logs just to see which tests have failed. I kinda see the
argument. At the same time, I wish nextest could do both: it could
aggregate logs of failed tests and then print out the summary.

Release Notes:

- N/A
2025-11-25 09:22:00 +00:00
AidanV
5139cc2bfb helix: Fix Vim::NextWordEnd off-by-one in HelixSelect (#43234)
Closes #43209
Closes #38121

Starting on the first character.
Running `v e` before changes: 
<img width="410" height="162" alt="image"
src="https://github.com/user-attachments/assets/ee13fa29-826c-45c0-9ea0-a598cc8e781a"
/>

Running `v e` after changes:
<img width="483" height="166" alt="image"
src="https://github.com/user-attachments/assets/24791a07-97df-47cd-9ef2-171522adb796"
/>

Change Notes:

- Added helix selection sanitation code that directly mirrors the code
in the Vim
[`visual_motion`](b6728c080c/crates/vim/src/visual.rs (L237))
method. I kept the comments from the Vim section that explains its
purpose.
- The above change converted the problem from fixing `v e` to fixing `v
w`. Since `w` is treated differently in Helix than in Vim (i.e. `w` in
Vim goes to the first character of a word and `w` in Helix goes to the
character before a word. Commented
[here](b6728c080c/crates/vim/src/helix.rs (L132))),
the code treats `w` in `HelixSelect` as a motion that differs from the
Vim motion in the same way that the function
[`helix_move_cursor`](b6728c080c/crates/vim/src/helix.rs (L353))
separates these behaviors.
- Added a regression test

Release Notes:

- Fixes bug where `Vim::NextWordEnd` in `HelixSelect` would not select
whole word.
2025-11-25 10:20:01 +01:00
Piotr Osiewicz
c0e85481b0 lsp: Fix potential double didClose notification when renaming a file (#43448)
Closes #42709

Release Notes:

- N/A
2025-11-25 09:11:43 +00:00
Kirill Bulatov
e6fe95b4f2 Only show ssh logs when toggled (#43445)
Same as in collab projects.

Release Notes:

- N/A
2025-11-25 08:25:49 +00:00
Kirill Bulatov
303c23cf1e Fix first window open not focusing the modals (#43180)
Closes https://github.com/zed-industries/zed/issues/4357
Closes https://github.com/zed-industries/zed/issues/41278

Release Notes:

- Fixed modals not getting focus on window reopen

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-25 07:34:23 +00:00
Ole Jørgen Brønner
0e2041dd41 multi_buffer: Fix editor::ExpandExcerpts failing when cursor is at excerpt start (#42324)
The bug is easily verified by:

1. open any multi-buffer
2. place the cursor at the beginning of an excerpt
3. run the editor::ExpandExcerpts / editor: expand excerpts action
4. The excerpt is not expanded

Since the `buffer_ids_for_range` function basically did the same and had
even been changed the same way earlier I DRYed these functions as well.

Note: I'm a rust novice, so keep an extra eye on rust technicalities
when reviewing :)

---

Release Notes:

- Fix editor: expand excerpts failing when cursor is at excerpt start

---------

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-11-25 08:21:18 +01:00
Max Brunsfeld
9122dd2d70 Combine zeta and zeta2 edit prediction providers (#43284)
We've realized that a lot of the logic within an
`EditPredictionProvider` is not specific to a particular edit prediction
model / service. Rather, it is just the generic state management
required to perform edit predictions at all in Zed. We want to move to a
setup where there's one "built-in" edit prediction provider in Zed,
which can be pointed at different edit prediction models. The only logic
that is different for different models is how we construct the prompt,
send the request, and parse the output.

This PR also changes the behavior of the staff-only `zeta2` feature flag
so that in only gates your *ability* to use Zeta2, but you can still use
your local settings file to choose between different edit prediction
models/services: zeta1, zeta2, and sweep.

This PR also makes zeta1's outcome reporting and prediction-rating
features work with all prediction models, not just zeta1.

To do:
* [x] remove duplicated logic around sending cloud requests between
zeta1 and zeta2
* [x] port the outcome reporting logic from zeta to zeta2.
* [x] get the "rate completions" modal working with all EP models
   * [x] display edit prediction diff
   * [x] show edit history events
* [x] remove the original `zeta` crate.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-24 22:17:48 -08:00
Kirill Bulatov
17d7988ad4 Redact environment variables in server info view (#43436)
Follow-up of https://github.com/zed-industries/zed/pull/42831

Release Notes:

- N/A
2025-11-24 22:05:16 +00:00
Julia Ryan
8fd2e2164c Fix remote project snippet duplication (#43429)
Closes #43311

Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2025-11-24 21:54:18 +00:00
Kirill Bulatov
e499f157dd Keep single default PHP language server (#43432)
9a119b18ee/extension.toml
provides 3 language servers for `php`, so `...` will always include all
3 if those are not excluded or included explicitly.

Change the configs and docs so, that only one php language server is
used.

Release Notes:

- N/A
2025-11-24 23:46:55 +02:00
Julia Ryan
f75e7582e6 Fix zed cli in NixOS WSL instances (#43433)
This fixes running `zed <path>` inside nixos wsl instances. We're
copying the approach used elsewhere which is to try using `--exec`
first, and if that fails use an actual shell which should cover the
nixos case because it only puts binaries on your PATH inside the
`/etc/profile` script which is sourced on shell startup.

Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2025-11-24 15:46:13 -06:00
Mayank Verma
9e69ac889c editor: Fix copy file actions not working in remote environments (#43362)
Closes #42500

Release Notes:

- Fixed all three editor actions not working in remote environments
  - `editor: copy file name`
  - `editor: copy file location`
  - `editor: copy file name without extension`

Here's the before/after:




https://github.com/user-attachments/assets/bfb03e99-2e1a-47a2-bd26-280180154fe3
2025-11-24 21:45:12 +00:00
Lennart
769464762a vim: Fix cursor shape after deactivation (#42834)
Update the `Vim.deactivate` method to ensure that the cursor shape is
reset to the one available in the user's settings, in the `cursor_shape`
setting, instead of simply defaulting to `CursorShape::Bar`.

In order to test this behavior, the `Editor.cursor_shape` method was
also introduced.

Release Notes:

- Fixed the cursor shape reset in vim mode deactivation, ensuring that
the user's `cursor_shape` setting is used

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-11-24 21:31:20 +00:00
Mayank Verma
342eba6f22 project: Send LSP metadata to remote ServerInfo (#42831)
Closes #39582

Release Notes:

- Added LSP metadata to remote ServerInfo

Here's the before/after:


https://github.com/user-attachments/assets/1057faa5-82af-4975-abad-5e10e139fac1

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-11-24 23:16:35 +02:00
Mikayla Maki
bd2c1027fa Add support for Opus 4.5 (#43425)
Adds support for Opus 4.5
- [x] BYOK
- [x] Amazon Bedrock

Release Notes:

- Added support for Opus 4.5

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-11-24 20:01:43 +00:00
localcc
d295ff4f04 Improve Windows path canonicalization (#43423)
Path canonicalization on windows will now favor keeping the drive letter
intact when canonicalizing paths. This helps some lsps with mapped
network drive compatibility.

Closes #41336 

Release Notes:

- N/A
2025-11-24 20:48:16 +01:00
morgankrey
7ce4f2ae62 Opus 4.5 and Gemini 3 to docs (#43424)
Add Opus 4.5 and Gemini 3 to docs

Release Notes:

- N/A
2025-11-24 13:38:14 -06:00
Kunall Banerjee
092250b4fa Rework and consolidate issue templates (#43403)
We’re reworking our triage process and in doing so, reworking our issue
templates is worth looking into. We have multiple issue templates, for
arbitrary categories, and not enough enforcement. The plan is to
consolidate the issue templates (maybe all into one) and drop the
others.

Release Notes:

- N/A
2025-11-24 14:12:54 -05:00
Yeoh Joer
b577f8a5ea Passthrough env to npm subcommands when using the system node runtime (#43102)
Closes #39448
Closes #37866

This PR expands the env-clearing fix from #42587 to include the
SystemNodeRuntime, which covers Node.js installations managed by Mise.
When running under the system runtime, npm subcommands were still
launched with a cleared environment, preventing variables such as
MISE_DATA_DIR from reaching the shim or the mise binary itself. As a
result, Mise finds the npm binary in the default MISE_DATA_DIR,
consistent with the behavior described in
https://github.com/zed-industries/zed/issues/39448#issuecomment-3433644569.

This change ensures that environment variables are passed through for
npm subcommands when using the system Node runtime, restoring expected
behavior for Mise-managed Node installations. This also fixes cases
where envs are used by npm itself.

Release Notes:

- Enable environment passthrough for npm subcommands
2025-11-24 11:08:45 -08:00
Danilo Leal
4329a817aa ui: Update ThreadItem component design (#43421)
Release Notes:

- N/A
2025-11-24 18:31:13 +00:00
Richard Feldman
6631d8be4e Fix Gemini 3 on OpenRouter (#43416)
Release Notes:

- Gemini 3 now works on OpenRouter in the Agent Panel
2025-11-24 13:24:26 -05:00
Agus Zubiaga
a7fff59136 Add each panel to the workspace as soon as it's ready (#43414)
We'll now add panels to the workspace as soon as they're ready rather
than waiting for all the rest to complete. We should strive to make all
panels fast, but given that their load tasks are fallible and do IO,
this approach seems more resilient.

Additionally, we'll now start loading the agent panel at the same time
as the rest.

Release Notes:

- workspace: Add panels as soon as they are ready
2025-11-24 14:41:40 -03:00
AidanV
4a36f67f94 vim: Fix bug where d . . freezes the editor (#42145)
This bug seems to be caused by pushing an operator (i.e. `d`) followed
by a repeat (i.e. `.`) so the recording includes the push operator and
the repeat. When this is repeated (i.e. `.`) it causes an infinite loop.

This change fixes this bug by pushing a ClearOperator action if there is
an ongoing recording when repeat is called.

Release Notes:

- Fixed bug where pressing `d . .` in Vim mode would freeze the editor.

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-11-24 16:55:19 +00:00
HuaGu-Dragon
47e8946581 Attempt to fix go to the end of the line when using helix mode (#41575)
Closes #41550

Release Notes:

- Fixed `<g-l>` behavior in helix mode which will now correctly go to the last charactor of the line.
- Fixed not switching to helix normal mode when in default vim context and pressing escape.

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-11-24 17:32:30 +01:00
Oleksiy Syvokon
ea7568ceb3 zeta2: Support experimental 1120-seedcoder model (#43411)
1. Introduce a common `PromptFormatter` trait
2. Let models define their generation params.
3. Add support for the experimental 1120-seedcoder prompt format


Release Notes:

- N/A
2025-11-24 16:27:11 +00:00
Kirill Bulatov
e6b42a2be2 Use a proper name for highlights.scm (#43412)
Release Notes:

- N/A
2025-11-24 16:15:38 +00:00
Piotr Osiewicz
7bbc65ea71 auto_updater: Fix upload-nightly.ps1 and auto-update check (#43404)
Release Notes:

- N/A
2025-11-24 16:39:17 +01:00
Danilo Leal
d6c550c838 debugger_ui: Add button to close the panel when docked to bottom (#43409)
This PR adds a button to close the panel when it is docked to the
bottom. Effectively, the button triggers the same `ToggleBottomDock`
action that clicking on the button that opened the panel triggers, but I
think having it there just makes it extra obvious how to close it, which
is beneficial.

As a bonus, also fixed the panel controls container height when it is
docked to the sides, so it perfectly aligns with the panel tabbar
height.

| Perfectly Aligned Header | Close Button |
|--------|--------|
| <img width="2620" height="2010" alt="Screenshot 2025-11-24 at 12  01
2@2x"
src="https://github.com/user-attachments/assets/08a50858-1b50-4ebd-af7a-c5dae32cf4f6"
/> | <img width="2620" height="2010" alt="Screenshot 2025-11-24 at 12 
01@2x"
src="https://github.com/user-attachments/assets/17a6eee0-9934-4949-8741-fffd5b106e95"
/> |

Release Notes:

- N/A
2025-11-24 12:28:23 -03:00
Danilo Leal
eff592c447 agent_ui: Refine "reject"/"keep" behavior when regenerating previous prompts (#43347)
Closes https://github.com/zed-industries/zed/issues/42753

Consider the following flow: you submit prompt A. Prompt A generates
some edits. You don't click on either "reject" or "keep"; they stay in a
pending state. You then submit prompt B, but before the agent outputs
any response, you click to edit prompt B, thus submitting a
regeneration.

Before this PR, the above flow would make the edits originated from
prompt A to be auto-rejected. This feels very incorrect and can surprise
users when they see that the edits that were pending got rejected. It
feels more correct to only auto-reject changes if you're regenerating
the prompt that directly generated those edits in the first place. Then,
it also feels more correct to assume that if there was a follow-up
prompt after some edits were made, those edits were passively
"accepted".

So, this is what this PR is doing. Consider the following flow to get a
picture of the behavior change:
- You submit prompt A. 
- Prompt A generates some edits. 
- You don't click on either "reject" or "keep"; they're pending. 
- You then submit prompt B, but before the agents outputs anything, you
click to edit prompt B, submitting a regeneration.
- Now, edits from prompt A will be auto-kept.

Release Notes:

- agent: Improved the "reject"/"keep" behavior when regenerating older
prompts by auto-keeping pending edits that don't originate from the
prompt to-be-regenerated.
2025-11-24 11:13:38 -03:00
Vasyl Protsiv
138286f3b1 sum_tree: Make SumTree::append run in logarithmic time (#43349)
The `SumTree::append` method is slow when appending large trees to small
trees. The reason is this code here:

f57f4cd360/crates/sum_tree/src/sum_tree.rs (L628-L630)

`append` is called recursively until `self` and `other` have the same
height, effectively making this code `O(log^2 n)` in the number of
leaves of `other` tree in the worst case.

There are no algorithmic reasons why appending large trees must be this
much slower.

This PR proves it by providing implementation of `append` that works in
logarithmic time regardless if `self` is smaller or larger than `other`.

The helper method `append_large` has the symmetric logic to
`push_tree_recursive` but moves the (unlikely) case of merging
underflowing node in a separate helper function to reduce stack usage. I
am a bit unsure about some implementation choices made in
`push_tree_recursive` and would like to discuss some of these later, but
at the moment I didn't change anything there and tried to follow the
same logic in `append_large`.

We might also consider adding `push_front`/`prepend` methods to
`SumTree`.

I did not find a good benchmark that covers this case so I added a new
one to rope benchmarks.

<details>
<summary>cargo bench (compared to current main)</summary>

```
     Running benches\rope_benchmark.rs (D:\zed\target\release\deps\rope_benchmark-59c669d2895cd2c4.exe)
Gnuplot not found, using plotters backend
push/4096               time:   [195.67 µs 195.75 µs 195.86 µs]
                        thrpt:  [19.944 MiB/s 19.955 MiB/s 19.964 MiB/s]
                 change:
                        time:   [+0.2162% +0.3040% +0.4057%] (p = 0.00 < 0.05)
                        thrpt:  [-0.4040% -0.3030% -0.2157%]
                        Change within noise threshold.
Found 14 outliers among 100 measurements (14.00%)
  2 (2.00%) low mild
  6 (6.00%) high mild
  6 (6.00%) high severe
Benchmarking push/65536: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 7.8s, enable flat sampling, or reduce sample count to 50.
push/65536              time:   [1.4431 ms 1.4485 ms 1.4546 ms]
                        thrpt:  [42.966 MiB/s 43.147 MiB/s 43.310 MiB/s]
                 change:
                        time:   [-3.2257% -1.2013% +0.6431%] (p = 0.27 > 0.05)
                        thrpt:  [-0.6390% +1.2159% +3.3332%]
                        No change in performance detected.
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) low mild
  5 (5.00%) high mild
  5 (5.00%) high severe

append/4096             time:   [15.107 µs 15.128 µs 15.149 µs]
                        thrpt:  [257.86 MiB/s 258.22 MiB/s 258.58 MiB/s]
                 change:
                        time:   [+0.9650% +1.5256% +1.9057%] (p = 0.00 < 0.05)
                        thrpt:  [-1.8701% -1.5026% -0.9557%]
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low mild
  1 (1.00%) high severe
append/65536            time:   [1.2870 µs 1.4496 µs 1.6484 µs]
                        thrpt:  [37.028 GiB/s 42.106 GiB/s 47.425 GiB/s]
                 change:
                        time:   [-28.699% -16.073% -0.3133%] (p = 0.04 < 0.05)
                        thrpt:  [+0.3142% +19.151% +40.250%]
                        Change within noise threshold.
Found 17 outliers among 100 measurements (17.00%)
  1 (1.00%) high mild
  16 (16.00%) high severe

slice/4096              time:   [30.580 µs 30.611 µs 30.639 µs]
                        thrpt:  [127.49 MiB/s 127.61 MiB/s 127.74 MiB/s]
                 change:
                        time:   [-2.2958% -0.9674% -0.1835%] (p = 0.08 > 0.05)
                        thrpt:  [+0.1838% +0.9769% +2.3498%]
                        No change in performance detected.
slice/65536             time:   [614.86 µs 795.04 µs 1.0293 ms]
                        thrpt:  [60.723 MiB/s 78.613 MiB/s 101.65 MiB/s]
                 change:
                        time:   [-12.714% +7.2092% +30.676%] (p = 0.52 > 0.05)
                        thrpt:  [-23.475% -6.7244% +14.566%]
                        No change in performance detected.
Found 14 outliers among 100 measurements (14.00%)
  14 (14.00%) high severe

bytes_in_range/4096     time:   [3.3298 µs 3.3416 µs 3.3563 µs]
                        thrpt:  [1.1366 GiB/s 1.1416 GiB/s 1.1456 GiB/s]
                 change:
                        time:   [+2.0652% +3.0667% +4.3765%] (p = 0.00 < 0.05)
                        thrpt:  [-4.1930% -2.9754% -2.0234%]
                        Performance has regressed.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high severe
bytes_in_range/65536    time:   [80.640 µs 80.825 µs 81.024 µs]
                        thrpt:  [771.38 MiB/s 773.28 MiB/s 775.05 MiB/s]
                 change:
                        time:   [-0.6566% +1.0994% +2.9691%] (p = 0.27 > 0.05)
                        thrpt:  [-2.8835% -1.0875% +0.6609%]
                        No change in performance detected.
Found 10 outliers among 100 measurements (10.00%)
  2 (2.00%) high mild
  8 (8.00%) high severe

chars/4096              time:   [763.17 ns 763.68 ns 764.36 ns]
                        thrpt:  [4.9907 GiB/s 4.9952 GiB/s 4.9985 GiB/s]
                 change:
                        time:   [-2.1138% -0.7973% +0.1096%] (p = 0.18 > 0.05)
                        thrpt:  [-0.1095% +0.8037% +2.1595%]
                        No change in performance detected.
Found 10 outliers among 100 measurements (10.00%)
  1 (1.00%) low severe
  6 (6.00%) low mild
  3 (3.00%) high severe
chars/65536             time:   [12.479 µs 12.503 µs 12.529 µs]
                        thrpt:  [4.8714 GiB/s 4.8817 GiB/s 4.8910 GiB/s]
                 change:
                        time:   [-2.4451% -1.0638% +0.6633%] (p = 0.16 > 0.05)
                        thrpt:  [-0.6589% +1.0753% +2.5063%]
                        No change in performance detected.
Found 11 outliers among 100 measurements (11.00%)
  4 (4.00%) high mild
  7 (7.00%) high severe

clip_point/4096         time:   [63.148 µs 63.182 µs 63.229 µs]
                        thrpt:  [61.779 MiB/s 61.825 MiB/s 61.859 MiB/s]
                 change:
                        time:   [+1.0107% +2.1329% +4.2849%] (p = 0.02 < 0.05)
                        thrpt:  [-4.1088% -2.0883% -1.0006%]
                        Performance has regressed.
Found 5 outliers among 100 measurements (5.00%)
  4 (4.00%) high mild
  1 (1.00%) high severe
Benchmarking clip_point/65536: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 7.8s, enable flat sampling, or reduce sample count to 50.
clip_point/65536        time:   [1.2578 ms 1.2593 ms 1.2608 ms]
                        thrpt:  [49.573 MiB/s 49.631 MiB/s 49.690 MiB/s]
                 change:
                        time:   [+0.4881% +0.8942% +1.3488%] (p = 0.00 < 0.05)
                        thrpt:  [-1.3308% -0.8863% -0.4857%]
                        Change within noise threshold.
Found 15 outliers among 100 measurements (15.00%)
  1 (1.00%) high mild
  14 (14.00%) high severe

point_to_offset/4096    time:   [16.211 µs 16.235 µs 16.257 µs]
                        thrpt:  [240.28 MiB/s 240.61 MiB/s 240.97 MiB/s]
                 change:
                        time:   [-1.4913% +0.1685% +2.2662%] (p = 0.89 > 0.05)
                        thrpt:  [-2.2159% -0.1682% +1.5139%]
                        No change in performance detected.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) high mild
  1 (1.00%) high severe
point_to_offset/65536   time:   [360.06 µs 360.58 µs 361.16 µs]
                        thrpt:  [173.05 MiB/s 173.33 MiB/s 173.58 MiB/s]
                 change:
                        time:   [+0.0939% +0.8792% +1.8751%] (p = 0.06 > 0.05)
                        thrpt:  [-1.8406% -0.8715% -0.0938%]
                        No change in performance detected.
Found 10 outliers among 100 measurements (10.00%)
  3 (3.00%) high mild
  7 (7.00%) high severe

cursor/4096             time:   [19.266 µs 19.282 µs 19.302 µs]
                        thrpt:  [202.38 MiB/s 202.58 MiB/s 202.75 MiB/s]
                 change:
                        time:   [+1.2457% +2.2477% +2.8702%] (p = 0.00 < 0.05)
                        thrpt:  [-2.7901% -2.1983% -1.2304%]
                        Performance has regressed.
Found 4 outliers among 100 measurements (4.00%)
  2 (2.00%) high mild
  2 (2.00%) high severe
cursor/65536            time:   [467.63 µs 468.36 µs 469.14 µs]
                        thrpt:  [133.22 MiB/s 133.44 MiB/s 133.65 MiB/s]
                 change:
                        time:   [-0.2019% +1.3419% +2.8915%] (p = 0.10 > 0.05)
                        thrpt:  [-2.8103% -1.3241% +0.2023%]
                        No change in performance detected.
Found 12 outliers among 100 measurements (12.00%)
  3 (3.00%) high mild
  9 (9.00%) high severe

append many/small to large
                        time:   [37.419 ms 37.656 ms 37.929 ms]
                        thrpt:  [321.84 MiB/s 324.17 MiB/s 326.22 MiB/s]
                 change:
                        time:   [+0.8113% +1.7361% +2.6538%] (p = 0.00 < 0.05)
                        thrpt:  [-2.5852% -1.7065% -0.8047%]
                        Change within noise threshold.
Found 9 outliers among 100 measurements (9.00%)
  9 (9.00%) high severe
append many/large to small
                        time:   [51.289 ms 51.437 ms 51.614 ms]
                        thrpt:  [236.50 MiB/s 237.32 MiB/s 238.00 MiB/s]
                 change:
                        time:   [-87.518% -87.479% -87.438%] (p = 0.00 < 0.05)
                        thrpt:  [+696.08% +698.66% +701.13%]
                        Performance has improved.
Found 13 outliers among 100 measurements (13.00%)
  4 (4.00%) high mild
  9 (9.00%) high severe
```
</details>

Release Notes:

- sum_tree: Make SumTree::append run in logarithmic time
2025-11-24 14:49:00 +01:00
Lukas Wirth
f6f8fc1229 gpui: Do not panic when GetMonitorInfoW fails (#43397)
Fixes ZED-29R

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-24 13:35:24 +00:00
Piotr Osiewicz
2d55c088cc releases: Add build number to Nightly builds (#42990)
- **Remove semantic_version crate and use semver instead**
- **Update upload-nightly**


Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-24 13:34:04 +01:00
Lukas Wirth
a0fa5d57c1 proto: Fix cloned errors losing all context (#43393)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-24 12:13:20 +00:00
Kunall Banerjee
f8729f6ea0 docs: Better wording for terminal.working_directory setting (#43388)
Initially this was just going to be a minor docs fix, but then I
wondered if we could improve the copy in the editor as well.

Release Notes:

- N/A
2025-11-24 11:24:05 +00:00
Lukas Wirth
f7772af197 util: Fix invalid powershell redirection syntax used in uni shell env capture (#43390)
Closes  https://github.com/zed-industries/zed/issues/42869

Release Notes:

- Fixed shell env sourcing not working with powershell on unix systems
2025-11-24 11:11:45 +00:00
Binlogo
2f46e6a43c http_client: Support GITHUB_TOKEN env to auth GitHub requests (#42623)
Closes #33903

Release Notes:

- Ensured Zed reuses `GITHUB_TOKEN` env variable when querying GitHub

---

Before fixing:

-  The `crates-lsp` extension request captured:
```
curl 'https://api.github.com/repos/MathiasPius/crates-lsp/releases' \
-H 'accept: */*' \
-H 'user-agent: Zed/0.212.3 (macos; aarch64)' \
-H 'host: api.github.com' \
```

-  `crates-lsp` extension error: 
```
Language server crates-lsp:

from extension "Crates LSP" version 0.2.0: status error 403, response: "{\"message\":\"API rate limit exceeded for x.x.x.x. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)\",\"documentation_url\":\"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting\"}\n"
```

After fixing:

```
export GITHUB_TOKEN=$(gh auth token)
cargo run
```

-  The `crates-lsp` extension request captured:
```
curl 'https://api.github.com/repos/MathiasPius/crates-lsp/releases' \
-H 'authorization: Bearer gho_Nt*****************2KXLw2' \
-H 'accept: */*' \
-H 'user-agent: Zed/0.214.0 (macos; aarch64)' \
-H 'host: api.github.com' \
```

The API rate limitation is resolved.

---

This isn't a perfect solution, but it enables users to avoid the noise.
2025-11-24 12:51:45 +02:00
Oscar Villavicencio
d333535e76 docs: Document git_hosting_providers for self-hosted Git instances (#43278)
Closes #38433

Document how to register self-hosted GitHub/GitLab/Bitbucket instances
via git_hosting_providers setting so permalinks and issue links resolve.

Release Notes:

- Added documentation on how to register self-hosted
GitHub/GitLab/Bitbucket instances via the `git_hosting_providers`
setting. This ensures permalinks and issue links can be resolved for
these instances.
2025-11-24 11:32:44 +01:00
shaik-zeeshan
4b04be6020 Fix gutter hover breakpoint not updating when switching the tabs (#43163)
Closes #42073

fixes hover breakpoint not disappearing from a tab when tabs are
switched


https://github.com/user-attachments/assets/43096d2a-cc5b-46c4-b903-5bc8c33305c5


Release Notes:

- N/A

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-11-24 10:27:42 +00:00
mg
fc11ecfa2b Add Windows path for extensions (#42645)
### Description

The `installing-extensions.md` guide was missing the directory path for
the Windows platform. It currently only lists the paths for macOS and
Linux. This PR adds the correct path for Windows users
(`%LOCALAPPDATA%\zed\extensions`).

Release Notes:

- N/A

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
2025-11-24 09:46:15 +00:00
Lukas Wirth
3281b9077f agent: Fix utf8 panic in outline (#43141)
Fixes ZED-3F3

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-24 09:43:20 +00:00
Benjamin Jurk
194f6c9f95 Treat .h++ files as C++ (#42802)
Release Notes:

- `.h++` files are now treated as C++.
2025-11-24 11:36:04 +02:00
Lukas Wirth
99277a427f miniprofiler_ui: Copy path to clipboard on click (#43280)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-24 10:33:03 +01:00
Ulysse Buonomo
48e113a90e cli: Allow opening non-existent paths (#43250)
Changes are made to `parse_path_with_position`:
we try to get the canonical, existing parts of
a path, then append the non-existing parts.

Closes #4441

Release Notes:

- Added the possibility to open a non-existing path using `zed` CLI
  ```
  zed path/to/non/existing/file.txt
  ```

Co-authored-by: Syed Sadiq Ali <sadiqonemail@gmail.com>
2025-11-24 11:30:19 +02:00
Danilo Leal
06f8e35597 agent_ui: Make thread markdown editable (#43377)
This PR makes the thread markdown editable. This refers to the "open
thread as markdown" feature, where you previously could only read. One
benefit of this move is that it makes a bit more obvious that you can
`cmd-s` to save the markdown, allowing you to store the content of a
given thread. You could already do this before, but due to it being
editable now, you see the tab with a dirty indicator, which communicates
that better.

Release Notes:

- agent: Made the thread markdown editable.
2025-11-24 00:12:04 -03:00
Danilo Leal
07b6686411 docs: Improve edit prediction page (#43379)
This PR improves the edit prediction page particularly by adding
information about pricing and plans, which wasn't at all mentioned here
before, _and_ by including a section with a keybinding example
demonstrating how to always use just `tab` to always accept edit
predictions.

Release Notes:

- N/A
2025-11-24 00:11:46 -03:00
Danilo Leal
dbcfb48198 Add mouse-based affordance to open a recent project in new window (#43373)
Closes https://github.com/zed-industries/zed/issues/31796

<img width="500" height="1034" alt="Screenshot 2025-11-23 at 7  39 2@2x"
src="https://github.com/user-attachments/assets/bd516359-328f-44aa-9130-33f9567df805"
/>

Release Notes:

- N/A
2025-11-23 19:40:33 -03:00
Ben Kunkle
34a2e1d56b settings_ui: Don't show sh as default shell on windows (#43276)
Closes #ISSUE

Release Notes:

- Fixed an issue in the settings UI where changing the terminal shell
would set the default shell to `sh` on Windows
2025-11-23 16:52:50 -05:00
Bennet Bo Fenner
da143c5527 Fix inline assist panic (#43364)
Fixes a panic that was introduced in #42633. Repro steps:
1. Open the inline assistant and mention a file in the prompt
2. Run the inline assistant
3. Remove the mention and insert a different one
4. 💥

This would happen because the mention set still had a reference to the
old editor, because we create a new one in `PromptEditor::unlink`.

Also removes the unused
`crates/agent_ui/src/context_picker/completion_provider.rs` file, which
was not removed by mistake in the previous PR.

Release Notes:

- N/A
2025-11-23 18:26:07 +01:00
Mayank Verma
1f03fc62db editor: Fix tab tooltips not showing file path for remote files (#43359)
Closes #42344

Release Notes:

- Fixed editor tab tooltips not showing file path for remote files

Here's the before/after, tested both local and remote:


https://github.com/user-attachments/assets/2768a0f8-e35b-4eff-aa95-d0decb51ec78
2025-11-23 17:35:43 +02:00
Lukas Wirth
06e03a41aa terminal_view: Reuse editor's blink manager (#43351)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-23 12:24:01 +00:00
John Tur
41c61900d1 Fix labels for GitHub issue templates (#43348)
Release Notes:

- N/A
2025-11-23 03:21:43 -05:00
Danilo Leal
f57f4cd360 agent_ui: Display footer for model selector when in Zed agent (#43294)
This PR adds back the footer with the "Configure" button in the model
selector but only when the seeing it from the Zed agent (or inline
assistant/text threads). I had removed it a while back because seeing
the "Configure" button, which takes you to the agent panel settings
view, when clicking from an external agent didn't make much sense, given
there's nothing model-wise you can configure from Zed (at least yet) for
an external agent.

This also makes the button in the footer a bit nicer by making it full
screen and displaying a keybinding, so that you can easily do the whole
"trigger model selector → go to settings view" all with the keyboard.

<img width="400" height="870" alt="Screenshot 2025-11-21 at 10  38@2x"
src="https://github.com/user-attachments/assets/c14f2acf-b793-4bc1-ac53-8a8a53b219e6"
/>

Release Notes:

- N/A
2025-11-23 00:33:18 -03:00
Danilo Leal
d9498b4b55 debugger_ui: Improve some elements of the UI (#43344)
- In the launch tab of the new session mode, I've switched it to use the
`InputField` component instead given that had all that we needed
already. Allows for removing a good chunk of editor-related code
- Also in the launch tab, added support for keyboard navigation between
all of the elements there (dropdown, inputs, and switch component)
- Added some simple an empty state treatment for the breakpoint column
when there are none set


https://github.com/user-attachments/assets/a441aa8a-360b-4e38-839f-786315a8a235

Release Notes:

- debugger: Made the input elements within the launch tab in the new
session modal keyboard navigable˙.
2025-11-22 23:28:34 -03:00
Danilo Leal
7a5851e155 ui: Remove CheckboxWithLabel and improve Switch and Checkbox (#43343)
This PR finally removes the `CheckboxWithLabel` component, which is not
fully needed given the `Checkbox` can take a `label` method. Then, took
advantage of the opportunity to add more methods with regards to label
customization (position, size, and color) in both the `Checkbox` and
`Switch` components.

Release Notes:

- N/A
2025-11-22 22:26:26 -03:00
warrenjokinen
5b23a4ad7b docs: Fix minor typo in docker.md (#43334)
Updated wording (added a missing word) for reporting issues in
Dockerfile extension documentation.

Closes #ISSUE N/A

Release Notes:

- N/A
2025-11-22 22:11:41 +02:00
Liffindra Angga Zaaldian
10eba0bd5f Update JavaScript default language server (#43316)
As stated in [TypeScript Language Server
documentation](https://zed.dev/docs/languages/typescript#language-servers),
JavaScript uses `vtsls` as the default language server.

Release Notes:

- N/A
2025-11-22 12:21:56 +02:00
Marco Mihai Condrache
ab0527b390 gpui: Fix documentation of window methods (#43315)
Closes #43313 

Release Notes:

- N/A

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-11-22 12:21:01 +02:00
Julia Ryan
bfe141ea79 Fix wsl path parsing (#43295)
Closes #40286

Release Notes:

- N/A

---------

Co-authored-by: John Tur <john-tur@outlook.com>
2025-11-22 02:23:52 +00:00
Cole Miller
4376eb8217 Disable flaky test_git_status_postprocessing test (#43293)
Release Notes:

- N/A
2025-11-22 01:45:01 +00:00
Lukas Wirth
8e2c0c3a0c askpass: Fix double command ampersand in powershell script (#43289)
Fixes https://github.com/zed-industries/zed/issues/42618 /
https://github.com/zed-industries/zed/issues/43109

Release Notes:

- N/A
2025-11-22 00:46:53 +00:00
Mikayla Maki
de58a496ef Fix a bug where Anthropic completions would not work on nightly (#43287)
Follow up to: https://github.com/zed-industries/zed/pull/43185/files

Release Notes:

- N/A

Co-authored-by: Michael <mbenfield@zed.dev>
2025-11-22 00:10:26 +00:00
Jakub Konka
d07193cdf2 git: Handle git pre-commit hooks separately (#43285)
We now run git pre-commit hooks before we commit. This ensures we don't
run into timeout issues with askpass delegate and report invalid error
to the user.

Closes #43157

Release Notes:

- Fixed long running pre-commit hooks causing committing from Zed to
fail.

Co-authored-by: Cole Miller <cole@zed.dev>
2025-11-22 00:33:32 +01:00
Conrad Irwin
279b76d440 Retry sentry uploads (#43267)
We see internal server errors occasionally; and it's very annoying to
have to re-run the entire step

Release Notes:

- N/A
2025-11-21 13:29:08 -07:00
Be
dfa102c5ae Add setting for enabling server-side decorations (#39250)
Previously, this was controllable via the undocumented
ZED_WINDOW_DECORATIONS environment variable (added in #13866). Using an
environment variable for this is inconvenient because it requires users
to set that environment variable somehow before starting Zed, such as in
the .desktop file or persistently in their shell. Controlling this via a
Zed setting is more convenient.

This does not modify the design of the titlebar in any way. It only
moves the existing option from an environment variable to a Zed setting.

Fixes #14165

Client-side decorations (default):
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/525feb92-2f60-47d3-b0ca-47c98770fa8c"
/>


Server-side decorations in KDE Plasma:
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/7379c7c8-e5e3-47ba-a3ea-4191fec9434d"
/>

Release Notes:

- Changed option for Wayland server-side decorations from an environment
variable to settings.json field

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-21 19:56:00 +00:00
Be
a04b3d80c8 gpui: Fall back to client-side decorations on Wayland if SSD not supported (#39313)
It is optional for Wayland servers to support server-side decorations.
In particular, GNOME chooses to not implement SSD
(https://gitlab.gnome.org/GNOME/mutter/-/issues/217). So, even if the
application requests SSD, it must draw client-side decorations unless
the application receives a response from the server confirming the
request for SSD.

Before, when the user requested SSD for Zed, but the Wayland server did
not support it, there were no server-side decorations (window titlebar)
drawn, but Zed did not draw the window minimize, maximize, and close
buttons either. This fixes Zed so it always draws the window control
buttons if the Wayland server does not support SSD.

Before on GNOME Wayland with SSD requested:
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/68a6d853-623d-401f-8e7f-21d4dea00543"
/>

After on GNOME Wayland with SSD requested:
<img width="3840" height="2160" alt="image"
src="https://github.com/user-attachments/assets/b258ae8b-fe0e-4ba2-a541-ef6f2c38f788"
/>


Release Notes:

- Fixed window control buttons not showing in GNOME Wayland when SSD
requested
2025-11-21 19:55:44 +00:00
Dave Waggoner
e76b485de3 terminal: New settings for path hyperlink regexes (#40305)
Closes:
- #12338
- #40202 

1. Adds two new settings which allow customizing the set of regexes used
to identify path hyperlinks in terminal
1. Fixes path hyperlinks for paths containing unicode emoji and
punctuation, for example, `mojo.🔥`
1. Fixes path hyperlinks for Windows verbatim paths, for example,
`\\?\C:\Over\here.rs`.
1. Improves path hyperlink performance, especially for terminals with a
lot of content
1. Replaces existing custom hard-coded default path hyperlink parsing
logic with a set of customizable default regexes

## New settings

(from default.json)

### terminal.path_hyperlink_regexes

Regexes used to identify paths for hyperlink navigation. Supports
optional named capture
groups `path`, `line`, `column`, and `link`. If none of these are
present, the entire match
is the hyperlink target. If `path` is present, it is the hyperlink
target, along with `line`
and `column` if present. `link` may be used to customize what text in
terminal is part of the
hyperlink. If `link` is not present, the text of the entire match is
used. If `line` and
`column` are not present, the default built-in line and column suffix
processing is used
which parses `line:column` and `(line,column)` variants. The default
value handles Python
diagnostics and common path, line, column syntaxes. This can be extended
or replaced to
handle specific scenarios. For example, to enable support for
hyperlinking paths which
contain spaces in rust output,
```
[
  "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
  "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
],
```
could be used. Processing stops at the first regex with a match, even if
no link is
produced which is the case when the cursor is not over the hyperlinked
text. For best
performance it is recommended to order regexes from most common to least
common. For
readability and documentation, each regex may be an array of strings
which are collected
into one multi-line regex string for use in terminal path hyperlink
detection.

### terminal.path_hyperlink_timeout_ms
Timeout for hover and Cmd-click path hyperlink discovery in
milliseconds. Specifying a
timeout of `0` will disable path hyperlinking in terminal.

## Performance

This PR fixes terminal to only search the hovered line for hyperlinks
and adds a benchmark. Before this fix, hyperlink detection grows
linearly with terminal content, with this fix it is proportional only to
the hovered line. The gains come from replacing
`visible_regex_match_iter`, which searched all visible lines, with code
that only searches the line hovered on (including if the line is
wrapped).

Local benchmark timings (terminal with 500 lines of content):

||main|this PR|Δ|
|-|-|-:|-|
| cargo_hyperlink_benchmark | 1.4 ms | 13 µs | -99.0% |
| rust_hyperlink_benchmark | 1.2 ms | 11 µs | -99.1% |
| ls_hyperlink_benchmark | 1.3 ms | 7 µs |  -99.5% |

Release Notes:

- terminal: New settings to allow customizing the set of regexes used to
identify path hyperlinks in terminal
- terminal: Fixed terminal path hyperlinks for paths containing unicode
punctuation and emoji, e.g. mojo.🔥
- terminal: Fixed path hyperlinks for Windows verbatim paths, for
example, `\\?\C:\Over\here.rs`
- terminal: Improved terminal hyperlink performance, especially for
terminals with a lot of content visible
2025-11-21 14:01:06 -05:00
Joseph T. Lyons
0492255d7b Make community champions public (#43271)
Release Notes:

- N/A
2025-11-21 18:30:02 +00:00
Agus Zubiaga
6b9c2b0363 zeta2: Improve jump outside UI (#43262)
Still a prototype UI but a bit more noticeable :) 

Release Notes:

- N/A
2025-11-21 17:22:49 +00:00
Bennet Bo Fenner
f0820ae8e4 agent_ui: Remove context strip from inline assistant (#42633)
TODO
- [x] Implement PromptEditor::paste
- [x] Fix creases on unlink
- [x] PromptCompletionProviderDelegate::supports_images
- [ ] Fix highlighting in completion menu

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-11-21 18:10:30 +01:00
Luke Naylor
5a9b810aef markdown: Add LaTeX syntax highlighting injection (#41110)
Closes [#30264](https://github.com/zed-industries/zed/issues/30264)

Small addition based on
[nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter/blob/main/runtime/queries/markdown_inline/injections.scm)

<img width="1122" height="356" alt="Screenshot From 2025-10-24 15-47-58"
src="https://github.com/user-attachments/assets/33e7387d-a299-4921-9db8-622d2657bec1"
/>

This does require the LaTeX extension to be installed.

Release Notes:

- Added LaTeX highlighting for inline and display equations in Markdown when the LaTeX extension is installed

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-21 16:57:56 +00:00
Agus Zubiaga
4fb671f4eb zeta2: Predict at next diagnostic location (#43257)
When no predictions are available for the current buffer, we will now
attempt to predict at the closest diagnostic from the cursor location
that wasn't included in the last prediction request. This enables a
commonly desired kind of far-away jump without requiring explicit model
support.

Release Notes:

- N/A
2025-11-21 16:39:08 +00:00
Lukas Wirth
a3cbe1a554 crashes: Print panic message to logs (#43159)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-21 17:31:57 +01:00
Conrad Irwin
9b823616dd Fix install linux (#43205)
Closes: #42726

Release Notes:

- Fix ./script/install-linux for installing a development version of Zed
on Linux
2025-11-21 09:12:19 -07:00
Smit Barmase
3c69e5c46b Revert "gpui: Convert macOS clipboard file URLs to paths for paste" (#43254)
Reverts zed-industries/zed#36848

Turns out this broke copying a screenshot from apps like CleanShot X and
then pasting it over. We should land this again after taking a look at
those cases. Pasting screenshots from the native macOS screenshot
functionality works though.

cc @seantimm 

Release Notes:

- Fixed issue where copying a screenshot from apps like CleanShot X into
Agent Panel didn't work as expected.
2025-11-21 16:11:19 +00:00
Conrad Irwin
2ac13b9489 Fallible Settings (#42938)
Also tidies up error notifications so that in the case of syntax errors
we don't see noise about the migration failing as well.

Release Notes:

- Invalid values in settings files will no longer prevent the rest of
the file from being parsed.
2025-11-21 08:28:17 -07:00
Lukas Wirth
a8d7f06b47 Revert "util: Check whether discovered powershell is actually executable" (#43247)
Reverts zed-industries/zed#43044
Closes https://github.com/zed-industries/zed/issues/43224

This slows down startup on windows significantly

Release Notes:

- Fixed slow startup on Windows
2025-11-21 14:48:41 +00:00
Smit Barmase
28e1c15e90 agent_ui: Fix sent agent prompt getting lost after authentication (#43245)
Closes #42379

Release Notes:

- Fixed issue where a sent agent message is not restored after
successful authentication.
2025-11-21 20:03:11 +05:30
Danilo Leal
0ee7271e48 Allow onboarding pages to be zoomed in/out (#43244)
We were just missing adding keybindings for these.

Release Notes:

- onboarding: The onboarding pages can now be zoomed in/out with the
same keybindings you'd use to zoom in/out a regular buffer.
2025-11-21 11:22:51 -03:00
Kunall Banerjee
d6a5566619 docs: Point to the right URL for Gemini CLI (#43239)
Point to the right URL for Gemini CLI.

Release Notes:

- N/A

---

💖
2025-11-21 07:23:43 -05:00
Kunall Banerjee
ea85f905f1 docs: Fix small typo in docs for Snippets (#43238)
Happened to notice this typo while going through the docs.

Release Notes:

- N/A

---

💖
2025-11-21 06:56:47 -05:00
Lukas Wirth
1ce58a88cc zed: Allocate more rayon threads depending on available parallelism (#43235)
While zed itself is not a heavy user of rayon, wasmtime is, especially
for compilation. This change is similar to the rayon default but we
halve the number of threads still so we don't spawn too many threads
overall.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-21 10:33:38 +00:00
Lukas Wirth
a30887f03b Fix some panics (#43233)
Fixes ZED-2NP
Fixes ZED-3DP
Fixes ZED-3EV

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-21 11:08:21 +01:00
jtaub
92b6e8eb6e Jetbrains keymap updates (#42848)
Closes https://github.com/zed-industries/zed/issues/14639

## Release Notes:

Various improvements to the Jetbrains keymap. Added various missing
keyboard shortcuts that I use on a daily basis in Jetbrains, and changed
a few which were present in the keymap but mapped to the wrong behavior.

### Added:
- Added various missing keybindings for Jetbrains keymap
  - `ctrl-n` → `project_symbols::Toggle`
  - `ctrl-alt-n` → `file_finder::Toggle` (open project files)
  - `ctrl-~` → `git::Branch`
  - `ctrl-\` → `assistant::InlineAssist`
  - `ctrl-space` → `editor::ShowCompletions`
  - `ctrl-q` → `editor::Hover`
  - `ctrl-p` → `editor::ShowSignatureHelp`
  - `ctrl-f5` → `task::Rerun`
  - `shift-f9` → `debugger::Start`
  - `shift-f10` → `task::Spawn`
- Added macOS equivalents for all of the above, however I only have a
Linux machine so I have not tested the mac bindings. The binding are
generally the same except `ctrl → cmd` with few exceptions.
  - `cmd-j` → `editor::Hover`

### Fixed:
- Several incorrectly mapped keybindings for the Jetbrains keymap
- `ctrl-alt-s` → `editor::OpenSettings` (was `editor::OpenSettingsFile`)
- `ctrl-alt-b` → `editor::GoToImplementation` (was
`editor::GoToDefinitionSplit`)
  - `alt-left` → `pane::ActivatePreviousItem`
  - `alt-right` → `pane::ActivateNextItem`
- `ctrl-k` now opens the Git panel. I believe this was commented out
because of a bug where focus is not given to the commit message text
box, but imo the current behavior of not doing anything at all feels
more confusing/frustrating to a Jetbrains user (projecting a little
here, happy to revert).
2025-11-21 11:04:43 +01:00
Andrew Farkas
0a6cb6117b Fix connect.host setting being ignored by debugpy (#43190)
Closes #42727

Unfortunately we can only support IPv4 addresses right now because
`TcpArguments` only supports an IPv4 address. I'm not sure how difficult
it would be to lift this limitation.

Release Notes:

- Fixed `connect.host` setting being ignored by debugpy

Co-authored-by: Cole Miller <cole@zed.dev>
2025-11-21 09:20:15 +00:00
Smit Barmase
e2f6422b3e language: Move language server update to background when it takes too long (#43164)
Closes https://github.com/zed-industries/zed/issues/42360

If updating a language server takes longer than 10 seconds, we now fall
back to launching the currently installed version (if exists) and
continue downloading the update in the background.

Release Notes:

- Improved language server updates for slow connection, now Zed launches
existing server if the update is taking too long.

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-11-21 14:19:55 +05:30
cacaosteve
bb514c158e macOS: Enumerate GPUs first; prefer low-power non-removable; fall back to system default (#38164)
Problem: Some macOS environments report no devices via
MTLCopyAllDevices, causing startup failure with “unable to access a
compatible graphics device,” especially on Apple Silicon.
Change: Prefer MTLCreateSystemDefaultDevice
(metal::Device::system_default()) first. If None, enumerate devices and
select a non‑removable, low‑power device by preference.
Why this works: On Apple Silicon the system default is the unified GPU;
on Intel, the fallback keeps a stable policy and avoids accidentally
picking removable/high‑power devices.
Impact: Fixes startup on affected ASi systems; improves selection
consistency on Intel multi‑GPU. Behavior unchanged where
system_default() succeeds.
Risk: Low. Aligns with Apple’s recommended selection path. Still fails
early with a clearer message if no Metal devices exist.

Closes #37689.

Release Notes:
- Fixed: Startup failure on some Apple Silicon machines when Metal
device enumeration returned no devices by falling back to the system
default device.

---------

Co-authored-by: 张小白 <364772080@qq.com>
Co-authored-by: Kate <work@localcc.cc>
2025-11-21 00:25:37 -05:00
Conrad Irwin
550442e100 Disable fsevents tests (#43218)
They're flakier than phyllo dough, and not nearly as delicious

Release Notes:

- N/A
2025-11-20 22:17:50 -07:00
Xipeng Jin
b3ebcef5c6 gpui: Only time out multi-stroke bindings when current prefix matches (#42659)
Part One for Resolving #10910

### Summary
Typing prefix (partial keybinding) will behave like Vim. No timeout
until you either finish the sequence or hit Escape, while ambiguous
sequences still auto-resolve after 1s.

### Description
This follow-up tweaks the which-key system first part groundwork so our
timeout behavior matches Vim’s expectations. Then we can implement the
UI part in the next step (reference latest comments in
https://github.com/zed-industries/zed/pull/34798)
- `DispatchResult` now reports when the current keystrokes are already a
complete binding in the active context stack (`pending_has_binding`). We
only start the 1s flush timer in that case. Pure prefixes or sequences
that only match in other contexts—stay pending indefinitely, so
leader-style combos like `space f g` no longer evaporate after a second.
- `Window::dispatch_key_event` cancels any prior timer before scheduling
a new one and only spawns the background flush task when
`pending_has_binding` is true. If there’s no matching binding, we keep
the pending keystrokes and rely on an explicit Escape or more typing to
resolve them.

Release Notes:

- Fixed multi-stroke keybindings so only ambiguous prefixes auto-trigger
after 1 s; unmatched prefixes now stay pending until canceled, matching
Vim-style leader behavior.
2025-11-20 19:42:56 -07:00
Conrad Irwin
2b9eeb9a30 Disable keychain timeout in bundle-mac (#43204)
Attempt to reduce the number of times bundle-mac fails to notorize by
disabling
keychain's auto-lock timeout

Release Notes:

- N/A
2025-11-21 02:42:49 +00:00
Max Brunsfeld
07d98981e8 Make the edit prediction status bar menu work correctly when using sweep (#43203)
Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-20 23:59:02 +00:00
Ben Kunkle
8bbd101dcd ci: Run check_docs when code changes (#43188)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-20 17:30:21 -05:00
Marshall Bowers
dbdc501c89 Fix casing in comments in default.json (#43201)
This PR fixes the casing of the operating system names in the
language-specific sections of `default.json`.

This files serves as documentation for users (since it can be viewed
through `zed: open default settings`), so we should make sure it is
tidy.

Release Notes:

- N/A
2025-11-20 22:17:52 +00:00
Mikayla Maki
898c133906 Simplify error management in stream_completion (#43035)
This PR simplifies error and event handling by removing the
`Ok(LanguageModelCompletionEvent::Status(CompletionRequestStatus::Failed)))`
state from the stream returned by `LanguageModel::stream_completion()`,
by changing it into an `Err(LanguageModelCompletionError)`. This was
done by collapsing the valid `CompletionRequestStatus` values into
`LanguageModelCompletionEvent`.

Release Notes:

- N/A

---------

Co-authored-by: Michael Benfield <mbenfield@zed.dev>
2025-11-20 22:16:07 +00:00
Michael Benfield
659169f06d Add codegen_ranges function in inline_assistant.rs (#43186)
Just a simple refactor.

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-11-20 21:57:43 +00:00
Danilo Leal
361fcc5c90 Make search field in panels be at the top (#43200)
This mostly affects the collab and outline panels for now. It has always
been a bit weird that the search field was at the bottom of the panel,
even more so because in both cases, you can _arrow down_ to start
navigating the list with your keyboard. So, with the search at the
bottom, you'd arrow down and get to the top of the list, which was very
strange. Now, with it at the top, it not only looks better but it is
also more generally consistent with other surfaces in the app, like
pickers, the settings UI, rules library, etc. Most search fields are
always at the top.

<img width="800" height="1830" alt="image"
src="https://github.com/user-attachments/assets/3e2c3b8f-5907-4d83-8804-b3fc77342103"
/>

Release Notes:

- N/A
2025-11-20 18:57:22 -03:00
Danilo Leal
9667d7882a extensions_ui: Improve error message when extensions fail to load (#43197)
<img width="500" height="1902" alt="Screenshot 2025-11-20 at 6  12@2x"
src="https://github.com/user-attachments/assets/daa5b020-17c8-4398-a64a-d691c566d6e7"
/>

Release Notes:

- extensions UI: Improved the feedback message for when extensions are
not being displayed due to a fetch error caused by lack of connection.
2025-11-20 18:28:11 -03:00
Danilo Leal
6adb0f4d03 agent_ui: Improve UI for the feedback container (#43195)
Improves a previously weird wrapping and simplify the UI by adding the
meta text inside the tooltip itself.


https://github.com/user-attachments/assets/9896d4a2-6954-4e61-9b77-864db8f2542a

Release Notes:

- N/A
2025-11-20 18:18:30 -03:00
Danilo Leal
a332b79189 ui: Add DiffStat component (#43192)
Release Notes:

- N/A
2025-11-20 18:18:08 -03:00
Xiaobo Liu
b41eb3cdaf windows: Fix maximized window size when DPI scale changes (#40053)
The WM_DPICHANGED suggested RECT is calculated for non-maximized
windows. When a maximized window's DPI changes, we now query the
monitor's work area directly to ensure the window correctly fills the
entire screen.

For non-maximized windows, the original behavior using the
system-suggested RECT is preserved.

Release Notes:

- windows: Fixed maximized window size when DPI scale changes

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
2025-11-20 20:34:17 +00:00
Andrew Farkas
6899448812 Remove prompt-caching-2024-07-31 beta header for Anthropic AI (#43185)
Closes #42715

Release Notes:

- Remove `prompt-caching-2024-07-31` beta header for Anthropic AI

Co-authored-by: Cole Miller <cole@zed.dev>
2025-11-20 15:16:09 -05:00
Lukas Wirth
28ef7455f0 gpui: #[inline] some trivial functions (#43189)
These appear in a lot of stacktraces (especially on windows) despite
them being plain forwarding calls.

Also removes some intermediate calls within gpui that will only turn
into more unnecessary compiler work.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-20 19:54:47 +00:00
Kirill Bulatov
7e341bcf94 Support bracket colorization (rainbow brackets) (#43172)
Deals with https://github.com/zed-industries/zed/issues/5259

Highlights brackets with different colors based on their depth.
Uses existing tree-sitter queries from brackets.scm to find brackets,
uses theme's accents to color them.


https://github.com/user-attachments/assets/cc5f3aba-22fa-446d-9af7-ba6e772029da

1. Adds `colorize_brackets` language setting that allows, per language
or globally for all languages, to configure whether Zed should color the
brackets for a particular language.

Disabled for all languages by default.

2. Any given language can opt-out a certain bracket pair by amending the
brackets.scm like `("\"" @open "\"" @close) ` -> `(("\"" @open "\""
@close) (#set! rainbow.exclude))`

3. Brackets are using colors from theme accents, which can be overridden
as

```jsonc
"theme_overrides": {
  "One Dark": {
    "accents": ["#ff69b4", "#7fff00", "#ff1493", "#00ffff", "#ff8c00", "#9400d3"]
  }
},
```

Release Notes:

- Added bracket colorization (rainbow brackets) support. Use
`colorize_brackets` language setting to enable.

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
Co-authored-by: MrSubidubi <finn@zed.dev>
Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-11-20 19:47:39 +00:00
Lukas Wirth
e6e5ccbf10 ui: Render fallback icon for avatars that failed to load (#43183)
Before we were simply not rendering anything which could lead to some
very surprising situations when joining channels ...

Now it will look like so
<img width="147" height="50" alt="image"
src="https://github.com/user-attachments/assets/13069de8-3dc0-45e1-b562-3fe81507dd87"
/>

Release Notes:

- Improved rendering of avatars that failed to load by rendering a
fallback icon
2025-11-20 19:30:34 +00:00
Andrew Farkas
d6d967f443 Re-resolve anchor before applying AI inline assist edits (#43103)
Closes #39088

Release Notes:

- Fixed AI assistant edits being scrambled when file was modified while
it was open

--

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-20 18:40:27 +00:00
Adrian
18f14a6ebf vim: Fix paste action for visual modes (#43031)
Closes #41810 

Release Notes:

- Fixed paste not working correctly in vim visual modes
2025-11-20 11:12:57 -07:00
Piotr Osiewicz
58fe19d55e project search: Skip loading of gitignored paths when their descendants will never match an inclusion/exclusion query (#42968)
Co-authored-by: dino <dinojoaocosta@gmail.com>

Related-to: #38799

Release Notes:

- Improved project search performance with "Also search files ignored by
configuration" combined with file inclusion/exclusion queries.

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
2025-11-20 18:44:55 +01:00
Bennet Bo Fenner
2a40dcfd77 acp: Support specifying settings for extensions (#43177)
This allows you to specify default_model and default_mode for ACP
extensions, e.g.
```
"auggie": {
  "default_model": "gpt-5",
  "default_mode": "default",
  "type": "extension"
},
```

Release Notes:

- Added support for specifying settings for ACP extensions
(`default_mode`, `default_model`)
2025-11-20 17:12:00 +00:00
Smit Barmase
a5c3267b3e extensions: Add - as linked edit character for HTML (#43179)
Closes https://github.com/zed-industries/zed/issues/43060

Release Notes:

- Fixed issue where typing in custom HTML tag would not complete
subsequent end tag for `-` character.

Co-authored-by: Kunall Banerjee <hey@kimchiii.space>
2025-11-20 17:05:24 +00:00
Dino
5ef6402d64 editor: Ensure all menus and popups are dismissed (#43169)
While investigating a bug report that, in Helix mode, pressing the
`escape` key would only hide the signature help popup and not the
completions menu, when `auto_signature_help` was enabled, it was noted
that the `editor::Editor.dismiss_menus_and_popups` method was not
dismissing all possible menus and popups and was, instead, stopping as
soon as a single menu or popup was dismissed.

From the name of the method it appears that we actually want to dismiss
all so this commit updates it as such, ensuring that the bug reported is
also fixed.

Closes #42499 

Release Notes:

- Fixed issue with popups and menus not being dismissed while using
`auto_signature_help` in Helix Mode
2025-11-20 16:25:09 +00:00
Danilo Leal
ba93a5d62f ui: Remove Badge component (#43168)
We're not using it anywhere anymore, so I think we can clean it up now.
This was a somewhat specific component we did for the sake of
Onboarding, but it ended up being removed.

Release Notes:

- N/A
2025-11-20 12:45:49 -03:00
Danilo Leal
73568fc454 ui: Add ThreadItem component (#43167)
Release Notes:

- N/A
2025-11-20 12:45:26 -03:00
Anthony Eid
56401fc99c debugger: Allow users to include PickProcessId in debug tasks and resolve Pid (#42913)
Closes #33286

This PR adds support for Zed's `$ZED_PICK_PID` command in debug
configurations, which allows users to select a process to attach to at
debug time. When this variable is present in a debug configuration, Zed
automatically opens a process picker modal.

Follow up for this will be integrating this variable in the task system
instead of just the debug configuration system.

Release Notes:

- Added `$ZED_PICK_PID` variable for debug configurations, allowing
users to select which process to attach the debugger to at runtime

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-11-20 10:12:59 -05:00
Vinh Tran
e033829ef2 Fix diff highlights (#38384)
Per
https://github.com/zed-industries/zed/discussions/23371#discussioncomment-13533635,
the issue is not new and I don't know how to solve the problem more
holistically yet.

All of the native themes don't have spec for `@diff.plus` and
`@diff.minus` leaving addition and deletion not being highlighted. For
diff file, the most valuable highlighting comes from exactly what we're
missing. Hence, I think this is worth fixing.

Perhaps, the ideal fix would be standardizing and documenting captures
such as `@diff.plus` and `@diff.minus` on
https://zed.dev/docs/extensions/languages#syntax-highlighting for theme
writers to adopt. But the existing list of captures seems to be
language-agnostic so I'm not sure if that's the best way forward.

Per
https://github.com/the-mikedavis/tree-sitter-diff/pull/18#issuecomment-2569785346,
`tree-sitter-diff`'s author prefers using `@keyword` and `@string` so
that `tree-sitter highlight` can work out of the box. So it seems to be
an ok choice for Zed.

Another approach is just adding `@diff.plus` and `@diff.minus` to the
native themes. Let me know if I should pursue this instead.

Before
<img width="668" height="328" alt="Screenshot 2025-09-18 at 11 16 14 AM"
src="https://github.com/user-attachments/assets/d9a5b3b5-b9ef-4e74-883f-831630fb431e"
/>

After
<img width="1011" height="404" alt="Screenshot 2025-09-18 at 12 11
15 PM"
src="https://github.com/user-attachments/assets/9cf453c0-30df-4d17-99e9-f2297865f12a"
/>
<img width="915" height="448" alt="Screenshot 2025-09-18 at 12 12 14 PM"
src="https://github.com/user-attachments/assets/9e7438a6-9009-4136-b841-1f8e1356bc9b"
/>



Closes https://github.com/zed-industries/extensions/issues/490


Release Notes:
- Fixed highlighting for addition and deletion for diff language

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-11-20 15:52:15 +01:00
Finn Evers
61f512af03 Move protobuf action to default linux runner (#43085)
Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-20 12:48:39 +00:00
Arun Chapagain
dd5482a899 docs: Update developing extension docs for updating specific submodule (#42548)
Release Notes:

- N/A
2025-11-20 13:33:13 +01:00
Bhuminjay Soni
9094eb811b git: Compress diff for commit message generation (#42835)
This PR compresses diff capped at 20000 bytes by:
- Truncation of all lines to 256 chars
- Iteratively removing last hunks from each file until size <= 20000
bytes.


Closes #34486

Release Notes:

- Improved: Compress large diffs for commit message generation (thanks
@11happy)

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-11-20 11:30:34 +00:00
Lukas Wirth
29f9853978 svg_preview: Remove unnecessary dependency on editor (#43147)
Editor is a choke point in our compilation graph while also being a very
common crate that is being edited. So reducing things that depend on it
will generally improve compilation times for us.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-20 12:18:50 +01:00
Aaron Saunders
1e45c99c80 Improve readability of files in the git changes panel (#41857)
Closes _unknown_

<img width="1212" height="463" alt="image"
src="https://github.com/user-attachments/assets/ec00fcf0-7eb9-4291-b1e2-66e014dc30ac"
/>


This PR places the file_name before the file_path so that when the panel
is slim it is still usable, mirrors the behaviour of the file picker
(cmd+P)

Release Notes:
-  Improved readability of files in the git changes panel
2025-11-20 06:14:46 -05:00
Danilo Leal
28f50977cf agent_ui: Add support for setting a model as the default for external agents (#43122)
This PR builds on top of the `default_mode` feature where it was
possible to set an external agent mode as the default if you held a
modifier while clicking on the desired option. Now, if you want to have,
for example, Haiku as your default Claude Code model, you can do that.
This feature adds parity between external agents and Zed's built-in one,
which already supported this feature for a little while.

Note: This still doesn't work with external agents installed from
extensions. At the moment, this is limited to Claude Code, Codex, and
Gemini—the ones we include out of the box.

Release Notes:

- agent: Added the ability to set a model as the default for a given
built-in external agent (Claude Code, Codex CLI, or Gemini CLI).
2025-11-20 11:00:01 +01:00
Lukas Wirth
95cb467cd9 multi_buffer: Remove redundant TypedOffset/TypedPoint (#43139)
Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-20 09:31:18 +00:00
Miguel Raz Guzmán Macedo
681a56506b project_panel: Add CollapseAllEntries keybinding (#43112)
Motivated by user feature requests

* https://github.com/zed-industries/zed/issues/6880
* https://discord.com/channels/869392257814519848/1439453067119562793

In analogy with VSCode functionality, we're adding a keybinding to the
project panel.

This is particularly for useful for large monorepos.

Release Notes:

- Keybinding added for `CollapseAllEntries` when in the `ProjectPanel`.

Co-authored-by: mikayla <mikayla@zed.dev>
2025-11-20 14:30:13 +05:30
Lukas Wirth
5052a460b4 vim: Fix increment panicking due to invalid utf8 offsets (#43101)
Fixes ZED-3ER

Release Notes:

- Fixed a panic when using vim increment on a multibyte character
2025-11-20 07:12:50 +00:00
Danilo Leal
66382acd52 ui: Remove outdated/unused component stories (#43118)
This PR removes basically all of the component stories, with the
exception of the context menu, which is a bit more intricate to set up.
All of the component that won't have a story after this PR will have an
entry in the Component Preview, which serves basically the same purpose.

Release Notes:

- N/A
2025-11-20 01:52:13 -03:00
Danilo Leal
ba596267d8 ui: Remove the ToggleButton component (#43115)
This PR removes the old `ToggleButton` component, replacing it with the
newer `ToggleButtonGroup` component in the couple of places that used to
use it. Ended up also adding a few more methods to the newer toggle
button group so the UI for the extensions page and the debugger main
picker didn't get visually impacted much. Then, as I was already in the
extensions page, decided to bake in some reasonably small UI
improvements to it as well.

Release Notes:

- N/A
2025-11-20 01:28:25 -03:00
Finn Evers
1fab43d467 Allow styling the container of markdown elements (#43107)
Closes #43033

Release Notes:

- FIxed an issue where the padding on info popovers would overlay text
when the content was scrollable.
2025-11-19 23:40:54 +00:00
Ben Kunkle
f2f40a5099 zeta2: Merge Sweep and Zeta2 Providers (#43097)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-11-19 15:40:06 -08:00
Piotr Osiewicz
c70f2d16ad lsp_button: Do not surface language servers from different windows in current workspace (#42733)
This led to a problem where we'd have a zombie entries in LSP dropdown
because they were treated as if they originated from an unknown
worktree.

Closes #42077

Release Notes:

- Fixed LSP status list containing zombie entries for LSPs in other
windows

---------

Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-11-19 22:40:17 +00:00
Lukas Wirth
c98b2d6944 multi_buffer: Typed MultiBufferOffset (#42707)
This PR introduces a new `MultiBufferOffset` new type wrapping size. The
goal of this is to make it clear at the type level when we are
interacting with offsets of a multi buffer versus offsets of a language
/ text buffer. This improves readability of things quite a bit by making
it clear what kind of offsets one is working with while also reducing
accidental bugs by using the wrong kin of offset for the wrong API.

This PR also uncovered two minor bugs due to that.

Does not yet introduce the MultiBufferPoint equivalent, that is for a
follow up PR.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-11-19 22:00:58 +00:00
Cole Miller
5e21457f21 Fix panic in the git panel when toggling sort_by_path (#43074)
We call `entry_by_path` on the `bulk_staging` anchor entry at the
beginning of `update_visible_entries`, but in the codepath where
`sort_by_path` is toggled on or off, we clear entries without clearing
`bulk_staging` or counts, causing that `entry_by_path` to do an out of
bounds index. Fixed by clearing `bulk_staging` as well.

Release Notes:

- N/A
2025-11-19 16:53:40 -05:00
Conrad Irwin
f312215e93 Potentially make zip test less flakey (#43099)
Authored-By: Claude

Release Notes:

- N/A
2025-11-19 14:50:32 -07:00
Jakub Konka
08692bb108 git: Clear pending ops for remote repos (#43098)
Release Notes:

- N/A
2025-11-19 22:47:51 +01:00
Max Brunsfeld
09e02a483a Allow running zeta evals against sweep (#43039)
This PR restructures the subcommands in `zeta-cli`, so that the
prediction engine (currently `zeta1` vs `zeta2`) is no longer the
highest order subcommand. Instead, there is just one layer of
subcommands: `eval`, `predict`, `context`, etc. Within these commands,
there are flags for using `zeta1`, `zeta2`, and now `sweep`.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2025-11-19 16:09:19 -05:00
David Kleingeld
68b87fc308 Use fixed calloop (#43081)
Calloop (used by our linux executor) was running all futures regardless
of how long they take. Unfortunaly some of our futures are rather busy
and take a while (>10ms).

Running all of them froze the editor for multiple seconds or even
minutes when opening a large project diff (git reset HEAD~2000 in
chromium for example).

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-11-19 22:06:13 +01:00
Agus Zubiaga
ec220dcc05 sweep: Coalesce edits based on line distance rather than time (#43006)
Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-11-19 20:32:14 +00:00
Joseph T. Lyons
b6c8c3f3d9 Remove migrated scripts (#43095)
These scripts have been migrated to:
https://github.com/zed-industries/release_notes

Release Notes:

- N/A
2025-11-19 20:28:40 +00:00
Smit Barmase
dccddf6f66 project_panel: Remove cmd-opt-. binding for hiding hidden files (#43091)
Lots of folks were accidentally clicking this. Even though it’s the
default in macOS Finder, it’s a good idea to not have it as the default
for us.

Release Notes:

- Removed the default `cmd-opt-.` binding for toggling hidden files in
the Project Panel so it’s harder to hide them by accident.
2025-11-20 00:38:29 +05:30
Andrew Farkas
27cb01f4af Fix Helix mode search & selection (#42928)
This PR redoes the desired behavior changes of #41583 (reverted in
#42892) but less invasively

Closes #41125
Closes #41164

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-11-19 18:30:55 +00:00
513 changed files with 23438 additions and 24544 deletions

View File

@@ -1,59 +0,0 @@
name: Bug Report (AI)
description: Zed Agent Panel Bugs
type: "Bug"
labels: ["ai"]
title: "AI: <a short description of the AI Related bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
### Model Provider Details
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
- Model Name:
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- Other Details (MCPs, other settings, etc):
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true
- 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,53 +0,0 @@
name: Bug Report (Debugger)
description: Zed Debugger-Related Bugs
type: "Bug"
labels: ["debugger"]
title: "Debugger: <a short description of the Debugger bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true
- 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,53 +0,0 @@
name: Bug Report (Git)
description: Zed Git Related Bugs
type: "Bug"
labels: ["git"]
title: "Git: <a short description of the Git bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one-line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true
- 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,53 +0,0 @@
name: Bug Report (Windows)
description: Zed Windows Related Bugs
type: "Bug"
labels: ["windows"]
title: "Windows: <a short description of the Windows bug>"
body:
- type: textarea
attributes:
label: Summary
description: Describe the bug with a one-line summary, and provide detailed reproduction steps
value: |
<!-- Please insert a one-line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
Steps to trigger the problem:
1.
2.
3.
**Expected Behavior**:
**Actual Behavior**:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true
- 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

70
.github/ISSUE_TEMPLATE/1.bug-report.yml vendored Normal file
View File

@@ -0,0 +1,70 @@
name: Report an issue
description: Report an issue with Zed.
type: Bug
body:
- type: markdown
attributes:
value: |
Feature requests should be opened in [discussions](https://github.com/zed-industries/zed/discussions/new/choose).
Before opening a new issue, please do a [search](https://github.com/zed-industries/zed/issues) of existing issues and :+1: upvote the existing issue instead. This will help us maintain a proper signal-to-noise ratio.
If you need help with your own project, you can ask a question in our [Discord Support Forums](https://discord.com/invite/zedindustries).
- type: textarea
attributes:
label: Reproduction steps
description: A step-by-step description of how to reproduce the issue from a **clean Zed install**. Any code must be sufficient to reproduce (make sure to include context!). Include code as text, not just as a screenshot. **Issues with insufficient detail may be summarily closed**.
placeholder: |
1. Start Zed
2. Click X
3. Y will happen
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: |
A clear and concise description of what is the current behavior (screenshots, videos), vs. what you expected the behavior to be.
**Skipping this/failure to provide complete information will result in the issue being closed.**
placeholder: "Based on my reproduction steps above, when I click X, I expect this to happen, but instead Y happens."
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your Zed log file to this issue.
description: |
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
value: |
<details><summary>Zed.log</summary>
<!-- Paste your log inside the code block. -->
```log
```
</details>
validations:
required: false
- type: textarea
attributes:
label: If applicable, provide details about your model provider
placeholder: |
- Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc.)
- Model Name: (Claude Sonnet 4.5, Gemini 3 Pro, GPT-5)
- Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
- Other details (ACPs, MCPs, other settings, etc.):
validations:
required: false
- type: textarea
attributes:
label: Zed version and system specs
description: |
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
Memory: 36 GiB
Architecture: aarch64
validations:
required: true

View File

@@ -1,75 +0,0 @@
name: Bug Report (Other)
description: |
Something else is broken in Zed (exclude crashing).
type: "Bug"
body:
- type: textarea
attributes:
label: Summary
description: Provide a one sentence summary and detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Describe with sufficient detail to reproduce from a clean Zed install.
- Any code must be sufficient to reproduce (include context!)
- Include code as text, not just as a screenshot.
- Issues with insufficient detail may be summarily closed.
-->
DESCRIPTION_HERE
Steps to reproduce:
1.
2.
3.
4.
**Expected Behavior**:
**Actual Behavior**:
<!-- Before Submitting, did you:
1. Include settings.json, keymap.json, .editorconfig if relevant?
2. Check your Zed.log for relevant errors? (please include!)
3. Click Preview to ensure everything looks right?
4. Hide videos, large images and logs in ``` inside collapsible blocks:
<details><summary>click to expand</summary>
```json
```
</details>
-->
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: |
Open Zed, from the command palette select "zed: copy system specs into clipboard"
placeholder: |
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,50 +0,0 @@
name: Crash Report
description: Zed is Crashing or Hanging
type: "Crash"
body:
- type: textarea
attributes:
label: Summary
description: Summarize the issue with detailed reproduction steps
value: |
<!-- Begin your issue with a one sentence summary -->
SUMMARY_SENTENCE_HERE
### Description
<!-- Include all steps necessary to reproduce from a clean Zed installation. Be verbose -->
Steps to trigger the problem:
1.
2.
3.
Actual Behavior:
Expected Behavior:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed Version and System Specs
description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
placeholder: |
Output of "zed: copy system specs into clipboard"
validations:
required: true
- 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

@@ -0,0 +1,52 @@
name: Report a crash
description: Zed is crashing or freezing or hanging.
type: Crash
body:
- type: textarea
attributes:
label: Reproduction steps
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
placeholder: |
1. Start Zed
2. Perform an action
3. Zed crashes
validations:
required: true
- type: textarea
attributes:
label: Current vs. Expected behavior
description: |
Go into depth about what actions youre performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
**Skipping this/failure to provide complete information will result in the issue being closed.**
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
validations:
required: true
- type: textarea
attributes:
label: Zed version and system specs
description: |
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
Memory: 36 GiB
Architecture: aarch64
validations:
required: true
- type: textarea
attributes:
label: If applicable, attach your Zed log file to this issue
description: |
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette 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,9 +1,9 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Feature Request
- name: Feature request
url: https://github.com/zed-industries/zed/discussions/new/choose
about: To request a feature, open a new Discussion in one of the appropriate Discussion categories
- name: "Zed Discord"
url: https://zed.dev/community-links
about: Real-time discussion and user support
about: To request a feature, open a new discussion under one of the appropriate categories.
- name: Our Discord community
url: https://discord.com/invite/zedindustries
about: Join our Discord server for real-time discussion and user support.

View File

@@ -13,13 +13,65 @@ jobs:
steps:
- name: Check if author is a community champion and apply label
uses: actions/github-script@v7
env:
COMMUNITY_CHAMPIONS: |
0x2CA
5brian
5herlocked
abdelq
afgomez
AidanV
akbxr
AlvaroParker
artemevsevev
bajrangCoder
bcomnes
Be-ing
blopker
bobbymannino
CharlesChen0823
chbk
cppcoffee
davewa
ddoemonn
djsauble
fantacell
findrakecil
gko
huacnlee
imumesh18
jacobtread
jansol
jeffreyguenther
jenslys
jongretar
lemorage
lnay
marcocondrache
marius851000
mikebronner
ognevny
RemcoSmitsDev
romaninsh
Simek
someone13574
sourcefrog
suxiaoshao
Takk8IS
tidely
timvermeulen
valentinegb
versecafe
vitallium
warrenjokinen
ya7010
Zertsov
with:
script: |
const communityChampionBody = `${{ secrets.COMMUNITY_CHAMPIONS }}`;
const communityChampions = communityChampionBody
const communityChampions = process.env.COMMUNITY_CHAMPIONS
.split('\n')
.map(handle => handle.trim().toLowerCase());
.map(handle => handle.trim().toLowerCase())
.filter(handle => handle.length > 0);
let author;
if (context.eventName === 'issues') {

View File

@@ -77,7 +77,7 @@ jobs:
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
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
timeout-minutes: 3
check_extension:

View File

@@ -33,7 +33,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
@@ -80,7 +80,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
@@ -112,7 +112,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: pwsh
- name: steps::cleanup_cargo_config
if: always()

View File

@@ -51,7 +51,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: pwsh
- name: steps::cleanup_cargo_config
if: always()

View File

@@ -47,7 +47,7 @@ jobs:
}
check_pattern "run_action_checks" '^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/' -qP
check_pattern "run_docs" '^docs/' -qP
check_pattern "run_docs" '^(docs/|crates/.*\.rs)' -qP
check_pattern "run_licenses" '^(Cargo.lock|script/.*licenses)' -qP
check_pattern "run_nix" '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' -qP
check_pattern "run_tests" '^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))' -qvP
@@ -117,7 +117,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than.ps1 250
shell: pwsh
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: pwsh
- name: steps::cleanup_cargo_config
if: always()
@@ -166,7 +166,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than 250
shell: bash -euxo pipefail {0}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
@@ -200,7 +200,7 @@ jobs:
run: ./script/clear-target-dir-if-larger-than 300
shell: bash -euxo pipefail {0}
- name: steps::cargo_nextest
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
run: cargo nextest run --workspace --no-fail-fast
shell: bash -euxo pipefail {0}
- name: steps::cleanup_cargo_config
if: always()
@@ -493,7 +493,10 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.run_tests == 'true'
runs-on: self-mini-macos
runs-on: namespace-profile-16x32-ubuntu-2204
env:
GIT_AUTHOR_NAME: Protobuf Action
GIT_AUTHOR_EMAIL: ci@zed.dev
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

493
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -147,7 +147,6 @@ members = [
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/semantic_version",
"crates/session",
"crates/settings",
"crates/settings_json",
@@ -165,7 +164,6 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/sweep_ai",
"crates/codestral",
"crates/svg_preview",
"crates/system_specs",
@@ -203,7 +201,6 @@ members = [
"crates/zed_actions",
"crates/zed_env_vars",
"crates/zeta",
"crates/zeta2",
"crates/zeta_cli",
"crates/zlog",
"crates/zlog_settings",
@@ -382,7 +379,6 @@ rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_json = { path = "crates/settings_json" }
@@ -399,7 +395,6 @@ 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" }
@@ -437,7 +432,6 @@ zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" }
zeta = { path = "crates/zeta" }
zeta2 = { path = "crates/zeta2" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
@@ -609,7 +603,6 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
@@ -632,7 +625,7 @@ rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_derive = "1.0.221"
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
@@ -643,7 +636,6 @@ serde_json_lenient = { version = "0.2", features = [
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
serde_with = "3.4.0"
sha2 = "0.10"
shellexpand = "2.1.0"
shlex = "1.3.0"
@@ -721,6 +713,7 @@ wasmtime = { version = "29", default-features = false, features = [
"parallel-compilation",
] }
wasmtime-wasi = "29"
wax = "0.6"
which = "6.0.0"
windows-core = "0.61"
wit-component = "0.221"
@@ -784,6 +777,7 @@ features = [
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" }
calloop = { git = "https://github.com/zed-industries/calloop" }
[profile.dev]
split-debuginfo = "unpacked"
@@ -847,7 +841,6 @@ refineable = { codegen-units = 1 }
release_channel = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
rich_text = { codegen-units = 1 }
semantic_version = { codegen-units = 1 }
session = { codegen-units = 1 }
snippet = { codegen-units = 1 }
snippets_ui = { codegen-units = 1 }

View File

@@ -43,8 +43,7 @@
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
"ctrl-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
"ctrl-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -240,13 +239,11 @@
"ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl->": "agent::AddSelectionToThread",
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
@@ -323,17 +320,6 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
@@ -825,8 +811,7 @@
"context": "PromptEditor",
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"ctrl-alt-e": "agent::RemoveAllContext"
"ctrl-]": "agent::CycleNextInlineAssist"
}
},
{
@@ -866,6 +851,7 @@
"context": "ProjectPanel",
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"ctrl-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
@@ -1251,11 +1237,25 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
},
{
"context": "Welcome",
"use_key_equivalents": true,
"bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
}
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,

View File

@@ -49,8 +49,7 @@
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
"cmd-alt-.": "project_panel::ToggleHideHidden"
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -279,13 +278,11 @@
"cmd-alt-p": "agent::ManageProfiles",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"cmd->": "agent::AddSelectionToThread",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread",
@@ -366,18 +363,6 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AgentConfiguration",
"bindings": {
@@ -890,9 +875,7 @@
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-alt-e": "agent::RemoveAllContext",
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist"
}
@@ -936,6 +919,7 @@
"use_key_equivalents": true,
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"cmd-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry",
"cmd-n": "project_panel::NewFile",
"cmd-d": "project_panel::Duplicate",
@@ -1234,23 +1218,23 @@
}
},
{
"context": "RateCompletionModal",
"context": "RatePredictionsModal",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion",
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
"right": "zeta::PreviewPrediction"
}
},
{
"context": "RateCompletionModal > Editor",
"context": "RatePredictionsModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
"escape": "zeta::FocusPredictions",
"cmd-shift-enter": "zeta::ThumbsUpActivePrediction",
"cmd-shift-backspace": "zeta::ThumbsDownActivePrediction"
}
},
{
@@ -1356,11 +1340,25 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }],
"cmd-enter": "onboarding::Finish",
"alt-tab": "onboarding::SignIn",
"alt-shift-a": "onboarding::OpenAccount"
}
},
{
"context": "Welcome",
"use_key_equivalents": true,
"bindings": {
"cmd-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"cmd--": ["zed::DecreaseUiFontSize", { "persist": false }],
"cmd-0": ["zed::ResetUiFontSize", { "persist": false }]
}
},
{
"context": "InvalidBuffer",
"use_key_equivalents": true,

View File

@@ -41,8 +41,7 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu",
"ctrl-alt-.": "project_panel::ToggleHideHidden"
"shift-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -241,13 +240,11 @@
"shift-alt-p": "agent::ManageProfiles",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-alt-/": "agent::ToggleModelSelector",
"ctrl-shift-a": "agent::ToggleContextPicker",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
// "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
"shift-alt-escape": "agent::ExpandMessageEditor",
"ctrl-shift-.": "agent::AddSelectionToThread",
"shift-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
@@ -329,18 +326,6 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "ContextStrip",
"use_key_equivalents": true,
"bindings": {
"up": "agent::FocusUp",
"right": "agent::FocusRight",
"left": "agent::FocusLeft",
"down": "agent::FocusDown",
"backspace": "agent::RemoveFocusedContext",
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
@@ -838,8 +823,7 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"shift-alt-e": "agent::RemoveAllContext"
"ctrl-]": "agent::CycleNextInlineAssist"
}
},
{
@@ -880,6 +864,7 @@
"use_key_equivalents": true,
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"ctrl-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry",
"ctrl-n": "project_panel::NewFile",
"alt-n": "project_panel::NewDirectory",
@@ -1285,11 +1270,25 @@
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }],
"ctrl-enter": "onboarding::Finish",
"alt-shift-l": "onboarding::SignIn",
"shift-alt-a": "onboarding::OpenAccount"
}
},
{
"context": "Welcome",
"use_key_equivalents": true,
"bindings": {
"ctrl-=": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl-+": ["zed::IncreaseUiFontSize", { "persist": false }],
"ctrl--": ["zed::DecreaseUiFontSize", { "persist": false }],
"ctrl-0": ["zed::ResetUiFontSize", { "persist": false }]
}
},
{
"context": "GitWorktreeSelector || (GitWorktreeSelector > Picker > Editor)",
"use_key_equivalents": true,

View File

@@ -1,16 +1,18 @@
[
{
"bindings": {
"ctrl-alt-s": "zed::OpenSettingsFile",
"ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom
"ctrl-~": "git::Branch",
"ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause",
"f7": "debugger::StepInto",
"f8": "debugger::StepOver",
"shift-f8": "debugger::StepOut",
"f9": "debugger::Continue",
"shift-f9": "debugger::Start",
"alt-shift-f9": "debugger::Start"
}
},
@@ -46,7 +48,7 @@
"alt-f7": "editor::FindAllReferences",
"ctrl-alt-f7": "editor::FindAllReferences",
"ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
"ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
"ctrl-alt-b": "editor::GoToImplementation", // Conflicts with workspace::ToggleRightDock
"ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
@@ -70,7 +72,11 @@
"ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }],
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-g": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions"
"alt-enter": "editor::ToggleCodeActions",
"ctrl-space": "editor::ShowCompletions",
"ctrl-q": "editor::Hover",
"ctrl-p": "editor::ShowSignatureHelp",
"ctrl-\\": "assistant::InlineAssist"
}
},
{
@@ -94,9 +100,13 @@
"ctrl-shift-f12": "workspace::ToggleAllDocks",
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"alt-shift-f10": "task::Spawn",
"shift-f10": "task::Spawn",
"ctrl-f5": "task::Rerun",
"ctrl-e": "file_finder::Toggle",
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-n": "project_symbols::Toggle",
"ctrl-alt-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"ctrl-alt-shift-n": "project_symbols::Toggle",
@@ -133,7 +143,9 @@
"context": "Pane",
"bindings": {
"ctrl-alt-left": "pane::GoBack",
"ctrl-alt-right": "pane::GoForward"
"ctrl-alt-right": "pane::GoForward",
"alt-left": "pane::ActivatePreviousItem",
"alt-right": "pane::ActivateNextItem"
}
},
{
@@ -152,8 +164,6 @@
"bindings": {
"ctrl-shift-t": "workspace::NewTerminal",
"alt-f12": "workspace::CloseActiveDock",
"alt-left": "pane::ActivatePreviousItem",
"alt-right": "pane::ActivateNextItem",
"ctrl-up": "terminal::ScrollLineUp",
"ctrl-down": "terminal::ScrollLineDown",
"shift-pageup": "terminal::ScrollPageUp",

View File

@@ -5,12 +5,14 @@
"cmd-}": "pane::ActivateNextItem",
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
"shift-escape": null, // Unmap workspace::zoom
"cmd-~": "git::Branch",
"ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause",
"f7": "debugger::StepInto",
"f8": "debugger::StepOver",
"shift-f8": "debugger::StepOut",
"f9": "debugger::Continue",
"shift-f9": "debugger::Start",
"alt-shift-f9": "debugger::Start"
}
},
@@ -45,7 +47,7 @@
"alt-f7": "editor::FindAllReferences",
"cmd-alt-f7": "editor::FindAllReferences",
"cmd-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
"cmd-alt-b": "editor::GoToDefinitionSplit",
"cmd-alt-b": "editor::GoToImplementation",
"cmd-shift-b": "editor::GoToTypeDefinition",
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic",
@@ -68,7 +70,11 @@
"cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }],
"cmd-shift-o": "file_finder::Toggle",
"cmd-l": "go_to_line::Toggle",
"alt-enter": "editor::ToggleCodeActions"
"alt-enter": "editor::ToggleCodeActions",
"ctrl-space": "editor::ShowCompletions",
"cmd-j": "editor::Hover",
"cmd-p": "editor::ShowSignatureHelp",
"cmd-\\": "assistant::InlineAssist"
}
},
{
@@ -96,9 +102,13 @@
"cmd-shift-f12": "workspace::ToggleAllDocks",
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-alt-r": "task::Spawn",
"shift-f10": "task::Spawn",
"cmd-f5": "task::Rerun",
"cmd-e": "file_finder::Toggle",
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-n": "file_finder::Toggle",
"cmd-n": "project_symbols::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",
"cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol
@@ -135,7 +145,9 @@
"context": "Pane",
"bindings": {
"cmd-alt-left": "pane::GoBack",
"cmd-alt-right": "pane::GoForward"
"cmd-alt-right": "pane::GoForward",
"alt-left": "pane::ActivatePreviousItem",
"alt-right": "pane::ActivateNextItem"
}
},
{

View File

@@ -414,13 +414,20 @@
}
},
{
"context": "vim_mode == helix_normal && !menu",
"context": "VimControl && vim_mode == helix_normal && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode",
"i": "vim::HelixInsert",
"a": "vim::HelixAppend",
"ctrl-[": "editor::Cancel"
}
},
{
"context": "vim_mode == helix_select && !menu",
"bindings": {
"escape": "vim::SwitchToHelixNormalMode"
}
},
{
"context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu",
"bindings": {
@@ -470,6 +477,9 @@
"alt-p": "editor::SelectPreviousSyntaxNode",
"alt-n": "editor::SelectNextSyntaxNode",
"n": "vim::HelixSelectNext",
"shift-n": "vim::HelixSelectPrevious",
// Goto mode
"g e": "vim::EndOfDocument",
"g h": "vim::StartOfLine",

View File

@@ -175,6 +175,16 @@
//
// Default: true
"zoomed_padding": true,
// What draws Zed's window decorations (titlebar):
// 1. Client application (Zed) draws its own window decorations
// "client"
// 2. Display server draws the window decorations. Not supported by GNOME Wayland.
// "server"
//
// This requires restarting Zed for changes to take effect.
//
// Default: "client"
"window_decorations": "client",
// Whether to use the system provided dialogs for Open and Save As.
// When set to false, Zed will use the built-in keyboard-first pickers.
"use_system_path_prompts": true,
@@ -255,6 +265,12 @@
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// Whether to colorize brackets in the editor.
// (also known as "rainbow brackets")
//
// The colors that are used for different indentation levels are defined in the theme (theme key: `accents`).
// They can be customized by using theme overrides.
"colorize_brackets": false,
// When to show the scrollbar in the completion menu.
// This setting can take four values:
//
@@ -1316,7 +1332,10 @@
// "hunk_style": "staged_hollow"
// 2. Show unstaged hunks hollow and staged hunks filled:
// "hunk_style": "unstaged_hollow"
"hunk_style": "staged_hollow"
"hunk_style": "staged_hollow",
// Should the name or path be displayed first in the git view.
// "path_style": "file_name_first" or "file_path_first"
"path_style": "file_name_first"
},
// The list of custom Git hosting providers.
"git_hosting_providers": [
@@ -1422,7 +1441,7 @@
"default_height": 320,
// What working directory to use when launching the terminal.
// May take 4 values:
// 1. Use the current file's project directory. Will Fallback to the
// 1. Use the current file's project directory. Fallback to the
// first project directory strategy if unsuccessful
// "working_directory": "current_project_directory"
// 2. Use the first project in this workspace's directory
@@ -1576,7 +1595,59 @@
//
// Most terminal themes have APCA values of 40-70.
// A value of 45 preserves colorful themes while ensuring legibility.
"minimum_contrast": 45
"minimum_contrast": 45,
// Regexes used to identify paths for hyperlink navigation. Supports optional named capture
// groups `path`, `line`, `column`, and `link`. If none of these are present, the entire match
// is the hyperlink target. If `path` is present, it is the hyperlink target, along with `line`
// and `column` if present. `link` may be used to customize what text in terminal is part of the
// hyperlink. If `link` is not present, the text of the entire match is used. If `line` and
// `column` are not present, the default built-in line and column suffix processing is used
// which parses `line:column` and `(line,column)` variants. The default value handles Python
// diagnostics and common path, line, column syntaxes. This can be extended or replaced to
// handle specific scenarios. For example, to enable support for hyperlinking paths which
// contain spaces in rust output,
//
// [
// "\\s+(-->|:::|at) (?<link>(?<path>.+?))(:$|$)",
// "\\s+(Compiling|Checking|Documenting) [^(]+\\((?<link>(?<path>.+))\\)"
// ],
//
// could be used. Processing stops at the first regex with a match, even if no link is
// produced which is the case when the cursor is not over the hyperlinked text. For best
// performance it is recommended to order regexes from most common to least common. For
// readability and documentation, each regex may be an array of strings which are collected
// into one multi-line regex string for use in terminal path hyperlink detection.
"path_hyperlink_regexes": [
// Python-style diagnostics
"File \"(?<path>[^\"]+)\", line (?<line>[0-9]+)",
// Common path syntax with optional line, column, description, trailing punctuation, or
// surrounding symbols or quotes
[
"(?x)",
"# optionally starts with 0-2 opening prefix symbols",
"[({\\[<]{0,2}",
"# which may be followed by an opening quote",
"(?<quote>[\"'`])?",
"# `path` is the shortest sequence of any non-space character",
"(?<link>(?<path>[^ ]+?",
" # which may end with a line and optionally a column,",
" (?<line_column>:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?",
"))",
"# which must be followed by a matching quote",
"(?(<quote>)\\k<quote>)",
"# and optionally a single closing symbol",
"[)}\\]>]?",
"# if line/column matched, may be followed by a description",
"(?(<line_column>):[^ 0-9][^ ]*)?",
"# which may be followed by trailing punctuation",
"[.,:)}\\]>]*",
"# and always includes trailing whitespace or end of line",
"([ ]+|$)"
]
],
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
// timeout of `0` will disable path hyperlinking in terminal.
"path_hyperlink_timeout_ms": 1
},
"code_actions_on_format": {},
// Settings related to running tasks.
@@ -1818,7 +1889,7 @@
}
},
"PHP": {
"language_servers": ["phpactor", "!intelephense", "..."],
"language_servers": ["phpactor", "!intelephense", "!phptools", "..."],
"prettier": {
"allowed": true,
"plugins": ["@prettier/plugin-php"],
@@ -2059,15 +2130,15 @@
"dev": {
// "theme": "Andromeda"
},
// Settings overrides to use when using linux
// Settings overrides to use when using Linux.
"linux": {},
// Settings overrides to use when using macos
// Settings overrides to use when using macOS.
"macos": {},
// Settings overrides to use when using windows
// Settings overrides to use when using Windows.
"windows": {
"languages": {
"PHP": {
"language_servers": ["intelephense", "!phpactor", "..."]
"language_servers": ["intelephense", "!phpactor", "!phptools", "..."]
}
}
},

View File

@@ -96,9 +96,9 @@
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
"version_control.added": "#2EA048ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.deleted": "#78081Bff",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"conflict": "#dec184ff",
@@ -497,9 +497,9 @@
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#aaaaaaff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
"version_control.added": "#2EA048ff",
"version_control.modified": "#d3b020ff",
"version_control.deleted": "#e06c76ff",
"version_control.deleted": "#F85149ff",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",
"conflict.border": "#f4e7d1ff",

View File

@@ -347,13 +347,13 @@ impl ToolCall {
let buffer = buffer.await.log_err()?;
let position = buffer
.update(cx, |buffer, _| {
let snapshot = buffer.snapshot();
if let Some(row) = location.line {
let snapshot = buffer.snapshot();
let column = snapshot.indent_size_for_line(row).len;
let point = snapshot.clip_point(Point::new(row, column), Bias::Left);
snapshot.anchor_before(point)
} else {
Anchor::MIN
Anchor::min_for_buffer(snapshot.remote_id())
}
})
.ok()?;
@@ -2120,7 +2120,7 @@ impl AcpThread {
position: edits
.last()
.map(|(range, _)| range.end)
.unwrap_or(Anchor::MIN),
.unwrap_or(Anchor::min_for_buffer(buffer.read(cx).remote_id())),
}),
cx,
);

View File

@@ -197,6 +197,11 @@ pub trait AgentModelSelector: 'static {
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
None
}
/// Returns whether the model picker should render a footer.
fn should_render_footer(&self) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -50,9 +50,14 @@ impl Diff {
let hunk_ranges = {
let buffer = buffer.read(cx);
let diff = diff.read(cx);
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>()
diff.hunks_intersecting_range(
Anchor::min_for_buffer(buffer.remote_id())
..Anchor::max_for_buffer(buffer.remote_id()),
buffer,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>()
};
multibuffer.set_excerpts_for_path(
@@ -316,7 +321,12 @@ impl PendingDiff {
let buffer = self.new_buffer.read(cx);
let diff = self.diff.read(cx);
let mut ranges = diff
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx)
.hunks_intersecting_range(
Anchor::min_for_buffer(buffer.remote_id())
..Anchor::max_for_buffer(buffer.remote_id()),
buffer,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>();
ranges.extend(

View File

@@ -409,9 +409,11 @@ impl ActionLog {
let new_diff_base = new_diff_base.clone();
async move {
let mut unreviewed_edits = Patch::default();
for hunk in diff_snapshot
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer_snapshot)
{
for hunk in diff_snapshot.hunks_intersecting_range(
Anchor::min_for_buffer(buffer_snapshot.remote_id())
..Anchor::max_for_buffer(buffer_snapshot.remote_id()),
&buffer_snapshot,
) {
let old_range = new_diff_base
.offset_to_point(hunk.diff_base_byte_range.start)
..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end);
@@ -732,12 +734,10 @@ impl ActionLog {
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],
telemetry.clone(),
cx,
);
let buffer_ranges = vec![Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
)];
let reject = self.reject_edits_in_ranges(buffer, buffer_ranges, telemetry.clone(), cx);
async move {
reject.await.log_err();
@@ -2010,7 +2010,8 @@ mod tests {
// User accepts the single hunk
action_log.update(cx, |log, cx| {
log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, None, cx)
let buffer_range = Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id());
log.keep_edits_in_range(buffer.clone(), buffer_range, None, cx)
});
cx.run_until_parked();
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -2031,7 +2032,14 @@ mod tests {
// User rejects the hunk
action_log
.update(cx, |log, cx| {
log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], None, cx)
log.reject_edits_in_ranges(
buffer.clone(),
vec![Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
)],
None,
cx,
)
})
.await
.unwrap();

View File

@@ -23,6 +23,7 @@ gpui.workspace = true
language.workspace = true
project.workspace = true
proto.workspace = true
semver.workspace = true
smallvec.workspace = true
ui.workspace = true
util.workspace = true

View File

@@ -925,15 +925,15 @@ impl StatusItemView for ActivityIndicator {
#[cfg(test)]
mod tests {
use gpui::SemanticVersion;
use release_channel::AppCommitSha;
use semver::Version;
use super::*;
#[test]
fn test_version_tooltip_message() {
let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic(
SemanticVersion::new(1, 0, 0),
Version::new(1, 0, 0),
));
assert_eq!(message, "Version: 1.0.0");

View File

@@ -961,6 +961,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
fn watch(&self, cx: &mut App) -> Option<watch::Receiver<()>> {
Some(self.connection.0.read(cx).models.watch())
}
fn should_render_footer(&self) -> bool {
true
}
}
impl acp_thread::AgentConnection for NativeAgentConnection {

View File

@@ -182,6 +182,7 @@ impl DbThread {
crate::Message::Agent(AgentMessage {
content,
tool_results,
reasoning_details: None,
})
}
language_model::Role::System => {

View File

@@ -172,14 +172,14 @@ impl EditAgent {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX,
position: language::Anchor::max_for_buffer(buffer.read(cx).remote_id()),
}),
cx,
)
});
output_events_tx
.unbounded_send(EditAgentOutputEvent::Edited(
language::Anchor::MIN..language::Anchor::MAX,
Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()),
))
.ok();
})?;
@@ -187,7 +187,7 @@ impl EditAgent {
while let Some(event) = parse_rx.next().await {
match event? {
CreateFileParserEvent::NewTextChunk { chunk } => {
cx.update(|cx| {
let buffer_id = cx.update(|cx| {
buffer.update(cx, |buffer, cx| buffer.append(chunk, cx));
self.action_log
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
@@ -195,15 +195,18 @@ impl EditAgent {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX,
position: language::Anchor::max_for_buffer(
buffer.read(cx).remote_id(),
),
}),
cx,
)
});
buffer.read(cx).remote_id()
})?;
output_events_tx
.unbounded_send(EditAgentOutputEvent::Edited(
language::Anchor::MIN..language::Anchor::MAX,
Anchor::min_max_range_for_buffer(buffer_id),
))
.ok();
}
@@ -703,6 +706,7 @@ impl EditAgent {
role: Role::User,
content: vec![MessageContent::Text(prompt)],
cache: false,
reasoning_details: None,
});
// Include tools in the request so that we can take advantage of
@@ -1199,7 +1203,9 @@ mod tests {
project.read_with(cx, |project, _| project.agent_location()),
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX
position: language::Anchor::max_for_buffer(
cx.update(|cx| buffer.read(cx).remote_id())
),
})
);
@@ -1217,7 +1223,9 @@ mod tests {
project.read_with(cx, |project, _| project.agent_location()),
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX
position: language::Anchor::max_for_buffer(
cx.update(|cx| buffer.read(cx).remote_id())
),
})
);
@@ -1235,7 +1243,9 @@ mod tests {
project.read_with(cx, |project, _| project.agent_location()),
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX
position: language::Anchor::max_for_buffer(
cx.update(|cx| buffer.read(cx).remote_id())
),
})
);
@@ -1253,7 +1263,9 @@ mod tests {
project.read_with(cx, |project, _| project.agent_location()),
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX
position: language::Anchor::max_for_buffer(
cx.update(|cx| buffer.read(cx).remote_id())
),
})
);
@@ -1268,7 +1280,9 @@ mod tests {
project.read_with(cx, |project, _| project.agent_location()),
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MAX
position: language::Anchor::max_for_buffer(
cx.update(|cx| buffer.read(cx).remote_id())
),
})
);
}

View File

@@ -1081,6 +1081,7 @@ fn message(
role,
content: contents.into_iter().collect(),
cache: false,
reasoning_details: None,
}
}
@@ -1268,6 +1269,7 @@ impl EvalAssertion {
role: Role::User,
content: vec![prompt.into()],
cache: false,
reasoning_details: None,
}],
thinking_allowed: true,
..Default::default()
@@ -1594,6 +1596,7 @@ impl EditAgentTest {
role: Role::System,
content: vec![MessageContent::Text(system_prompt)],
cache: true,
reasoning_details: None,
}]
.into_iter()
.chain(eval.conversation)

View File

@@ -48,7 +48,7 @@ pub async fn get_buffer_content_or_outline(
if outline_items.is_empty() {
let text = buffer.read_with(cx, |buffer, _| {
let snapshot = buffer.snapshot();
let len = snapshot.len().min(1024);
let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(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}")
@@ -178,7 +178,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let content = "A".repeat(100 * 1024); // 100KB
let content = "".repeat(100 * 1024); // 100KB
let content_len = content.len();
let buffer = project
.update(cx, |project, cx| project.create_buffer(true, cx))
@@ -194,7 +194,7 @@ mod tests {
// Should contain some of the actual file content
assert!(
result.text.contains("AAAAAAAAAA"),
result.text.contains("⚡⚡⚡⚡⚡⚡⚡"),
"Result did not contain content subset"
);

View File

@@ -215,7 +215,8 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
vec![LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 1".into()],
cache: true
cache: true,
reasoning_details: None,
}]
);
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text(
@@ -239,17 +240,20 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 1".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec!["Response to Message 1".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 2".into()],
cache: true
cache: true,
reasoning_details: None,
}
]
);
@@ -295,37 +299,44 @@ async fn test_prompt_caching(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 1".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec!["Response to Message 1".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Message 2".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec!["Response to Message 2".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Use the echo tool".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)],
cache: true
cache: true,
reasoning_details: None,
}
]
);
@@ -648,25 +659,26 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["abc".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use.clone())],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result.clone())],
cache: true
cache: true,
reasoning_details: None,
},
]
);
// Simulate reaching tool use limit.
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached);
fake_model.end_last_completion_stream();
let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
assert!(
@@ -684,22 +696,26 @@ async fn test_resume_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["abc".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Continue where you left off".into()],
cache: true
cache: true,
reasoning_details: None,
}
]
);
@@ -749,9 +765,7 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
};
fake_model
.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone()));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::StatusUpdate(
cloud_llm_client::CompletionRequestStatus::ToolUseLimitReached,
));
fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached);
fake_model.end_last_completion_stream();
let last_event = events.collect::<Vec<_>>().await.pop().unwrap();
assert!(
@@ -773,22 +787,26 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["abc".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![MessageContent::ToolUse(tool_use)],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::ToolResult(tool_result)],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
content: vec!["ghi".into()],
cache: true
cache: true,
reasoning_details: None,
}
]
);
@@ -1831,7 +1849,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Hey!".into()],
cache: true
cache: true,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
@@ -1839,7 +1858,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
MessageContent::Text("Hi!".into()),
MessageContent::ToolUse(echo_tool_use.clone())
],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
@@ -1850,7 +1870,8 @@ async fn test_building_request_with_pending_tools(cx: &mut TestAppContext) {
content: "test".into(),
output: Some("test".into())
})],
cache: false
cache: false,
reasoning_details: None,
},
],
);
@@ -2248,12 +2269,14 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
LanguageModelRequestMessage {
role: Role::User,
content: vec!["Call the echo tool!".into()],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())],
cache: false
cache: false,
reasoning_details: None,
},
LanguageModelRequestMessage {
role: Role::User,
@@ -2266,7 +2289,8 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
output: Some("test".into())
}
)],
cache: true
cache: true,
reasoning_details: None,
},
]
);
@@ -2280,7 +2304,8 @@ async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) {
thread.last_message(),
Some(Message::Agent(AgentMessage {
content: vec![AgentMessageContent::Text("Done".into())],
tool_results: IndexMap::default()
tool_results: IndexMap::default(),
reasoning_details: None,
}))
);
})
@@ -2528,7 +2553,7 @@ fn setup_context_server(
let mut settings = ProjectSettings::get_global(cx).clone();
settings.context_servers.insert(
name.into(),
project::project_settings::ContextServerSettings::Custom {
project::project_settings::ContextServerSettings::Stdio {
enabled: true,
command: ContextServerCommand {
path: "somebinary".into(),

View File

@@ -15,7 +15,7 @@ use agent_settings::{
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use client::{ModelRequestUsage, RequestUsage, UserStore};
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
use collections::{HashMap, HashSet, IndexMap};
use fs::Fs;
use futures::stream;
@@ -113,6 +113,7 @@ impl Message {
role: Role::User,
content: vec!["Continue where you left off".into()],
cache: false,
reasoning_details: None,
}],
}
}
@@ -177,6 +178,7 @@ impl UserMessage {
role: Role::User,
content: Vec::with_capacity(self.content.len()),
cache: false,
reasoning_details: None,
};
const OPEN_CONTEXT: &str = "<context>\n\
@@ -444,6 +446,7 @@ impl AgentMessage {
role: Role::Assistant,
content: Vec::with_capacity(self.content.len()),
cache: false,
reasoning_details: self.reasoning_details.clone(),
};
for chunk in &self.content {
match chunk {
@@ -479,6 +482,7 @@ impl AgentMessage {
role: Role::User,
content: Vec::new(),
cache: false,
reasoning_details: None,
};
for tool_result in self.tool_results.values() {
@@ -508,6 +512,7 @@ impl AgentMessage {
pub struct AgentMessage {
pub content: Vec<AgentMessageContent>,
pub tool_results: IndexMap<LanguageModelToolUseId, LanguageModelToolResult>,
pub reasoning_details: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -1398,6 +1403,18 @@ impl Thread {
self.handle_thinking_event(text, signature, event_stream, cx)
}
RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx),
ReasoningDetails(details) => {
let last_message = self.pending_message();
// Store the last non-empty reasoning_details (overwrites earlier ones)
// This ensures we keep the encrypted reasoning with signatures, not the early text reasoning
if let serde_json::Value::Array(ref arr) = details {
if !arr.is_empty() {
last_message.reasoning_details = Some(details);
}
} else {
last_message.reasoning_details = Some(details);
}
}
ToolUse(tool_use) => {
return Ok(self.handle_tool_use_event(tool_use, event_stream, cx));
}
@@ -1430,20 +1447,16 @@ impl Thread {
);
self.update_token_usage(usage, cx);
}
StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => {
UsageUpdated { amount, limit } => {
self.update_model_request_usage(amount, limit, cx);
}
StatusUpdate(
CompletionRequestStatus::Started
| CompletionRequestStatus::Queued { .. }
| CompletionRequestStatus::Failed { .. },
) => {}
StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => {
ToolUseLimitReached => {
self.tool_use_limit_reached = true;
}
Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()),
Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()),
Stop(StopReason::ToolUse | StopReason::EndTurn) => {}
Started | Queued { .. } => {}
}
Ok(None)
@@ -1677,6 +1690,7 @@ impl Thread {
role: Role::User,
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
cache: false,
reasoning_details: None,
});
let task = cx
@@ -1687,9 +1701,7 @@ impl Thread {
let event = event.log_err()?;
let text = match event {
LanguageModelCompletionEvent::Text(text) => text,
LanguageModelCompletionEvent::StatusUpdate(
CompletionRequestStatus::UsageUpdated { amount, limit },
) => {
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => {
this.update(cx, |thread, cx| {
thread.update_model_request_usage(amount, limit, cx);
})
@@ -1743,6 +1755,7 @@ impl Thread {
role: Role::User,
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
cache: false,
reasoning_details: None,
});
self.pending_title_generation = Some(cx.spawn(async move |this, cx| {
let mut title = String::new();
@@ -1753,9 +1766,7 @@ impl Thread {
let event = event?;
let text = match event {
LanguageModelCompletionEvent::Text(text) => text,
LanguageModelCompletionEvent::StatusUpdate(
CompletionRequestStatus::UsageUpdated { amount, limit },
) => {
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => {
this.update(cx, |thread, cx| {
thread.update_model_request_usage(amount, limit, cx);
})?;
@@ -1992,6 +2003,7 @@ impl Thread {
role: Role::System,
content: vec![system_prompt.into()],
cache: false,
reasoning_details: None,
}];
for message in &self.messages {
messages.extend(message.to_request());

View File

@@ -275,7 +275,9 @@ impl AgentTool for ReadFileTool {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor.unwrap_or(text::Anchor::MIN),
position: anchor.unwrap_or_else(|| {
text::Anchor::min_for_buffer(buffer.read(cx).remote_id())
}),
}),
cx,
);

View File

@@ -35,6 +35,7 @@ pub struct AcpConnection {
auth_methods: Vec<acp::AuthMethod>,
agent_capabilities: acp::AgentCapabilities,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
root_dir: PathBuf,
// NB: Don't move this into the wait_task, since we need to ensure the process is
// killed on drop (setting kill_on_drop on the command seems to not always work).
@@ -57,6 +58,7 @@ pub async fn connect(
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
@@ -66,6 +68,7 @@ pub async fn connect(
command.clone(),
root_dir,
default_mode,
default_model,
is_remote,
cx,
)
@@ -82,6 +85,7 @@ impl AcpConnection {
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
@@ -207,6 +211,7 @@ impl AcpConnection {
sessions,
agent_capabilities: response.agent_capabilities,
default_mode,
default_model,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
@@ -245,6 +250,7 @@ impl AgentConnection for AcpConnection {
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let default_mode = self.default_mode.clone();
let default_model = self.default_model.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers =
@@ -333,6 +339,7 @@ impl AgentConnection for AcpConnection {
let default_mode = default_mode.clone();
let session_id = response.session_id.clone();
let modes = modes.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
@@ -367,6 +374,53 @@ impl AgentConnection for AcpConnection {
}
}
if let Some(default_model) = default_model {
if let Some(models) = models.as_ref() {
let mut models_ref = models.borrow_mut();
let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model);
if has_model {
let initial_model_id = models_ref.current_model_id.clone();
cx.spawn({
let default_model = default_model.clone();
let session_id = response.session_id.clone();
let models = models.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id: default_model,
meta: None,
})
.await.log_err();
if result.is_none() {
models.borrow_mut().current_model_id = initial_model_id;
}
}
}).detach();
models_ref.current_model_id = default_model;
} else {
let available_models = models_ref
.available_models
.iter()
.map(|model| format!("- `{}`: {}", model.model_id, model.name))
.collect::<Vec<_>>()
.join("\n");
log::warn!(
"`{default_model}` is not a valid {name} model. Available options:\n{available_models}",
);
}
} else {
log::warn!(
"`{name}` does not support model selection, but `default_model` was set in settings.",
);
}
}
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {

View File

@@ -68,6 +68,18 @@ pub trait AgentServer: Send {
) {
}
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
None
}
fn set_default_model(
&self,
_model_id: Option<agent_client_protocol::ModelId>,
_fs: Arc<dyn Fs>,
_cx: &mut App,
) {
}
fn connect(
&self,
root_dir: Option<&Path>,

View File

@@ -55,6 +55,27 @@ impl AgentServer for ClaudeCode {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.default_model = model_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -68,6 +89,7 @@ impl AgentServer for ClaudeCode {
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -90,6 +112,7 @@ impl AgentServer for ClaudeCode {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -56,6 +56,27 @@ impl AgentServer for Codex {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file(fs, cx, |settings, _| {
settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_model = model_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -69,6 +90,7 @@ impl AgentServer for Codex {
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -92,6 +114,7 @@ impl AgentServer for Codex {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -44,19 +44,63 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
.and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into())))
}
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, _| {
if let Some(settings) = settings
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.get_mut(&name)
{
settings.default_mode = mode_id.map(|m| m.to_string())
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
});
match settings {
settings::CustomAgentServerSettings::Custom { default_mode, .. }
| settings::CustomAgentServerSettings::Extension { default_mode, .. } => {
*default_mode = mode_id.map(|m| m.to_string());
}
}
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_model().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
});
match settings {
settings::CustomAgentServerSettings::Custom { default_model, .. }
| settings::CustomAgentServerSettings::Extension { default_model, .. } => {
*default_model = model_id.map(|m| m.to_string());
}
}
});
}
@@ -72,6 +116,7 @@ impl crate::AgentServer for CustomAgentServer {
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);
let default_model = self.default_model(cx);
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
@@ -98,6 +143,7 @@ impl crate::AgentServer for CustomAgentServer {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -476,6 +476,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
}),
gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings {
@@ -484,6 +485,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
}),
custom: collections::HashMap::default(),
},

View File

@@ -37,6 +37,7 @@ impl AgentServer for Gemini {
let store = delegate.store.downgrade();
let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
cx.spawn(async move |cx| {
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
@@ -69,6 +70,7 @@ impl AgentServer for Gemini {
command,
root_dir.as_ref(),
default_mode,
default_model,
is_remote,
cx,
)

View File

@@ -69,7 +69,6 @@ postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
ref-cast.workspace = true
release_channel.workspace = true
rope.workspace = true
rules_library.workspace = true
@@ -93,7 +92,6 @@ time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
watch.workspace = true
workspace.workspace = true
@@ -115,6 +113,7 @@ languages = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
semver.workspace = true
rand.workspace = true
tree-sitter-md.workspace = true
unindent.workspace = true

View File

@@ -1,4 +1,3 @@
mod completion_provider;
mod entry_view_state;
mod message_editor;
mod mode_selector;

View File

@@ -405,7 +405,7 @@ mod tests {
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
use editor::RowInfo;
use fs::FakeFs;
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
use gpui::{AppContext as _, TestAppContext};
use crate::acp::entry_view_state::EntryViewState;
use multi_buffer::MultiBufferRow;
@@ -539,7 +539,7 @@ mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@ use ui::{
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector};
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
pub struct ModeSelector {
connection: Rc<dyn AgentSessionModes>,
@@ -108,36 +108,11 @@ impl ModeSelector {
entry.documentation_aside(side, DocumentationEdge::Bottom, {
let description = description.clone();
move |cx| {
move |_| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(h_flex().flex_shrink_0().children(
ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
),
))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
} else {
this.child("to also set as default")
}
})),
)
.child(HoldForDefault::new(is_default))
.into_any_element()
}
})

View File

@@ -1,27 +1,38 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_servers::AgentServer;
use anyhow::Result;
use collections::IndexMap;
use fs::Fs;
use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
use gpui::{
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem,
ListItemSpacing, prelude::*,
};
use util::ResultExt;
use zed_actions::agent::OpenSettings;
use crate::ui::HoldForDefault;
pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
pub fn acp_model_selector(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<AcpModelSelector>,
) -> AcpModelSelector {
let delegate = AcpModelPickerDelegate::new(selector, window, cx);
let delegate =
AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems(20.))
@@ -35,17 +46,23 @@ enum AcpModelPickerEntry {
pub struct AcpModelPickerDelegate {
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
filtered_entries: Vec<AcpModelPickerEntry>,
models: Option<AgentModelList>,
selected_index: usize,
selected_description: Option<(usize, SharedString)>,
selected_description: Option<(usize, SharedString, bool)>,
selected_model: Option<AgentModelInfo>,
_refresh_models_task: Task<()>,
focus_handle: FocusHandle,
}
impl AcpModelPickerDelegate {
fn new(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<AcpModelSelector>,
) -> Self {
@@ -86,12 +103,15 @@ impl AcpModelPickerDelegate {
Self {
selector,
agent_server,
fs,
filtered_entries: Vec::new(),
models: None,
selected_model: None,
selected_index: 0,
selected_description: None,
_refresh_models_task: refresh_models_task,
focus_handle,
}
}
@@ -181,6 +201,21 @@ impl PickerDelegate for AcpModelPickerDelegate {
if let Some(AcpModelPickerEntry::Model(model_info)) =
self.filtered_entries.get(self.selected_index)
{
if window.modifiers().secondary() {
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
self.agent_server.set_default_model(
if is_default {
None
} else {
Some(model_info.id.clone())
},
self.fs.clone(),
cx,
);
}
self.selector
.select_model(model_info.id.clone(), cx)
.detach_and_log_err(cx);
@@ -225,6 +260,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
),
AcpModelPickerEntry::Model(model_info) => {
let is_selected = Some(model_info) == self.selected_model.as_ref();
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
let model_icon_color = if is_selected {
Color::Accent
@@ -239,8 +276,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
this
.on_hover(cx.listener(move |menu, hovered, _, cx| {
if *hovered {
menu.delegate.selected_description = Some((ix, description.clone()));
} else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) {
menu.delegate.selected_description = Some((ix, description.clone(), is_default));
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) {
menu.delegate.selected_description = None;
}
cx.notify();
@@ -283,14 +320,57 @@ impl PickerDelegate for AcpModelPickerDelegate {
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<ui::DocumentationAside> {
self.selected_description.as_ref().map(|(_, description)| {
let description = description.clone();
DocumentationAside::new(
DocumentationSide::Left,
DocumentationEdge::Top,
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
)
})
self.selected_description
.as_ref()
.map(|(_, description, is_default)| {
let description = description.clone();
let is_default = *is_default;
DocumentationAside::new(
DocumentationSide::Left,
DocumentationEdge::Top,
Rc::new(move |_| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(HoldForDefault::new(is_default))
.into_any_element()
}),
)
})
}
fn render_footer(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
if !self.selector.should_render_footer() {
return None;
}
Some(
h_flex()
.w_full()
.p_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("configure", "Configure")
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx);
}),
)
.into_any(),
)
}
}

View File

@@ -1,6 +1,9 @@
use std::rc::Rc;
use std::sync::Arc;
use acp_thread::{AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{
@@ -20,13 +23,25 @@ pub struct AcpModelSelectorPopover {
impl AcpModelSelectorPopover {
pub(crate) fn new(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle_clone = focus_handle.clone();
Self {
selector: cx.new(move |cx| acp_model_selector(selector, window, cx)),
selector: cx.new(move |cx| {
acp_model_selector(
selector,
agent_server,
fs,
focus_handle_clone.clone(),
window,
cx,
)
}),
menu_handle,
focus_handle,
}

View File

@@ -297,6 +297,7 @@ pub struct AcpThreadView {
_cancel_task: Option<Task<()>>,
_subscriptions: [Subscription; 5],
show_codex_windows_warning: bool,
in_flight_prompt: Option<Vec<acp::ContentBlock>>,
}
enum ThreadState {
@@ -437,6 +438,7 @@ impl AcpThreadView {
new_server_version_available: None,
resume_thread_metadata: resume_thread,
show_codex_windows_warning,
in_flight_prompt: None,
}
}
@@ -591,9 +593,13 @@ impl AcpThreadView {
.connection()
.model_selector(thread.read(cx).session_id())
.map(|selector| {
let agent_server = this.agent.clone();
let fs = this.project.read(cx).fs().clone();
cx.new(|cx| {
AcpModelSelectorPopover::new(
selector,
agent_server,
fs,
PopoverMenuHandle::default(),
this.focus_handle(cx),
window,
@@ -647,7 +653,6 @@ impl AcpThreadView {
mode_selector,
_subscriptions: subscriptions,
};
this.message_editor.focus_handle(cx).focus(window);
this.profile_selector = this.as_native_thread(cx).map(|thread| {
cx.new(|cx| {
@@ -1151,6 +1156,7 @@ impl AcpThreadView {
}
this.update_in(cx, |this, window, cx| {
this.in_flight_prompt = Some(contents.clone());
this.set_editor_is_expanded(false, cx);
this.scroll_to_bottom(cx);
this.message_editor.update(cx, |message_editor, cx| {
@@ -1178,7 +1184,12 @@ impl AcpThreadView {
})?;
let res = send.await;
let turn_time_ms = turn_start_time.elapsed().as_millis();
let status = if res.is_ok() { "success" } else { "failure" };
let status = if res.is_ok() {
this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
"success"
} else {
"failure"
};
telemetry::event!(
"Agent Turn Completed",
agent = agent_telemetry_id,
@@ -1261,6 +1272,28 @@ impl AcpThreadView {
};
cx.spawn_in(window, async move |this, cx| {
// Check if there are any edits from prompts before the one being regenerated.
//
// If there are, we keep/accept them since we're not regenerating the prompt that created them.
//
// If editing the prompt that generated the edits, they are auto-rejected
// through the `rewind` function in the `acp_thread`.
let has_earlier_edits = thread.read_with(cx, |thread, _| {
thread
.entries()
.iter()
.take(entry_ix)
.any(|entry| entry.diffs().next().is_some())
})?;
if has_earlier_edits {
thread.update(cx, |thread, cx| {
thread.action_log().update(cx, |action_log, cx| {
action_log.keep_all_edits(None, cx);
});
})?;
}
thread
.update(cx, |thread, cx| thread.rewind(user_message_id, cx))?
.await?;
@@ -4070,7 +4103,9 @@ impl AcpThreadView {
action_log
.reject_edits_in_ranges(
buffer.clone(),
vec![Anchor::MIN..Anchor::MAX],
vec![Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
)],
Some(telemetry.clone()),
cx,
)
@@ -4091,7 +4126,9 @@ impl AcpThreadView {
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer.clone(),
Anchor::MIN..Anchor::MAX,
Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
),
Some(telemetry.clone()),
cx,
);
@@ -4710,11 +4747,8 @@ impl AcpThreadView {
let buffer = multibuffer.as_singleton();
if agent_location.buffer.upgrade() == buffer {
let excerpt_id = multibuffer.excerpt_ids().first().cloned();
let anchor = editor::Anchor::in_buffer(
excerpt_id.unwrap(),
buffer.unwrap().read(cx).remote_id(),
agent_location.position,
);
let anchor =
editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([anchor..anchor]);
})
@@ -4763,7 +4797,7 @@ impl AcpThreadView {
buffer.update(cx, |buffer, cx| {
buffer.set_text(markdown, cx);
buffer.set_language(Some(markdown_language), cx);
buffer.set_capability(language::Capability::ReadOnly, cx);
buffer.set_capability(language::Capability::ReadWrite, cx);
})?;
workspace.update_in(cx, |workspace, window, cx| {
@@ -5020,15 +5054,12 @@ impl AcpThreadView {
}));
let mut container = h_flex()
.id("thread-controls-container")
.group("thread-controls-container")
.w_full()
.py_2()
.px_5()
.gap_px()
.opacity(0.6)
.hover(|style| style.opacity(1.))
.flex_wrap()
.hover(|s| s.opacity(1.))
.justify_end();
if AgentSettings::get_global(cx).enable_feedback
@@ -5038,23 +5069,13 @@ impl AcpThreadView {
{
let feedback = self.thread_feedback.feedback;
container = container
.child(
div().visible_on_hover("thread-controls-container").child(
Label::new(match feedback {
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
Some(ThreadFeedback::Negative) => {
"We appreciate your feedback and will use it to improve."
}
None => {
"Rating the thread sends all of your current conversation to the Zed team."
}
})
.color(Color::Muted)
.size(LabelSize::XSmall)
.truncate(),
),
let tooltip_meta = || {
SharedString::new(
"Rating the thread sends all of your current conversation to the Zed team.",
)
};
container = container
.child(
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square)
@@ -5063,7 +5084,12 @@ impl AcpThreadView {
Some(ThreadFeedback::Positive) => Color::Accent,
_ => Color::Ignored,
})
.tooltip(Tooltip::text("Helpful Response"))
.tooltip(move |window, cx| match feedback {
Some(ThreadFeedback::Positive) => {
Tooltip::text("Thanks for your feedback!")(window, cx)
}
_ => Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx),
})
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
})),
@@ -5076,7 +5102,16 @@ impl AcpThreadView {
Some(ThreadFeedback::Negative) => Color::Accent,
_ => Color::Ignored,
})
.tooltip(Tooltip::text("Not Helpful"))
.tooltip(move |window, cx| match feedback {
Some(ThreadFeedback::Negative) => {
Tooltip::text(
"We appreciate your feedback and will use it to improve in the future.",
)(window, cx)
}
_ => {
Tooltip::with_meta("Not Helpful Response", None, tooltip_meta(), cx)
}
})
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
})),
@@ -5689,6 +5724,11 @@ impl AcpThreadView {
provider_id: None,
};
this.clear_thread_error(cx);
if let Some(message) = this.in_flight_prompt.take() {
this.message_editor.update(cx, |editor, cx| {
editor.set_message(message, window, cx);
});
}
let this = cx.weak_entity();
window.defer(cx, |window, cx| {
Self::handle_auth_required(this, err, agent, connection, window, cx);
@@ -6066,8 +6106,9 @@ pub(crate) mod tests {
use acp_thread::StubAgentConnection;
use agent_client_protocol::SessionId;
use assistant_text_thread::TextThreadStore;
use editor::MultiBufferOffset;
use fs::FakeFs;
use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext};
use gpui::{EventEmitter, TestAppContext, VisualTestContext};
use project::Project;
use serde_json::json;
use settings::SettingsStore;
@@ -6584,7 +6625,7 @@ pub(crate) mod tests {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init(semver::Version::new(0, 0, 0), cx);
prompt_store::init(cx)
});
}
@@ -7234,7 +7275,7 @@ pub(crate) mod tests {
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([8..15]);
selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
});
editor
@@ -7296,7 +7337,7 @@ pub(crate) mod tests {
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([8..15]);
selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
});
editor

View File

@@ -12,7 +12,7 @@ use client::zed_urls;
use cloud_llm_client::{Plan, PlanV1, PlanV2};
use collections::HashMap;
use context_server::ContextServerId;
use editor::{Editor, SelectionEffects, scroll::Autoscroll};
use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll};
use extension::ExtensionManifest;
use extension_host::ExtensionStore;
use fs::Fs;
@@ -1343,11 +1343,12 @@ async fn open_new_agent_servers_entry_in_settings_editor(
.custom
.insert(
server_name,
settings::CustomAgentServerSettings {
settings::CustomAgentServerSettings::Custom {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
default_mode: None,
default_model: None,
},
);
}
@@ -1362,7 +1363,15 @@ async fn open_new_agent_servers_entry_in_settings_editor(
.map(|(range, _)| range.clone())
.collect::<Vec<_>>();
item.edit(edits, cx);
item.edit(
edits.into_iter().map(|(range, s)| {
(
MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
s,
)
}),
cx,
);
if let Some((unique_server_name, buffer)) =
unique_server_name.zip(item.buffer().read(cx).as_singleton())
{
@@ -1375,7 +1384,9 @@ async fn open_new_agent_servers_entry_in_settings_editor(
window,
cx,
|selections| {
selections.select_ranges(vec![range]);
selections.select_ranges(vec![
MultiBufferOffset(range.start)..MultiBufferOffset(range.end),
]);
},
);
}

View File

@@ -182,7 +182,7 @@ impl ConfigurationSource {
parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
(
id,
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: true,
command,
},
@@ -403,7 +403,7 @@ impl ConfigureContextServerModal {
window.spawn(cx, async move |cx| {
let target = match settings {
ContextServerSettings::Custom {
ContextServerSettings::Stdio {
enabled: _,
command,
} => Some(ConfigurationTarget::Existing {
@@ -635,7 +635,6 @@ 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,
@@ -712,12 +711,12 @@ impl ConfigureContextServerModal {
)
} else if let ConfigurationSource::New { is_http, .. } = &self.source {
let label = if *is_http {
"Run command"
"Configure Local"
} else {
"Connect via HTTP"
"Configure Remote"
};
let tooltip = if *is_http {
"Configure an MCP serevr that runs on stdin/stdout."
"Configure an MCP server that runs on stdin/stdout."
} else {
"Configure an MCP server that you connect to over HTTP"
};

View File

@@ -253,6 +253,7 @@ impl ManageProfilesModal {
});
},
false, // Do not use popover styles for the model picker
self.focus_handle.clone(),
window,
cx,
)

View File

@@ -13,8 +13,8 @@ use editor::{
scroll::Autoscroll,
};
use gpui::{
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
Action, AnyElement, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, Focusable,
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -145,7 +145,7 @@ impl AgentDiffPane {
let diff_hunk_ranges = diff
.hunks_intersecting_range(
language::Anchor::MIN..language::Anchor::MAX,
language::Anchor::min_max_range_for_buffer(snapshot.remote_id()),
&snapshot,
cx,
)
@@ -580,11 +580,11 @@ impl Item for AgentDiffPane {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}

View File

@@ -25,6 +25,8 @@ impl AgentModelSelector {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle_clone = focus_handle.clone();
Self {
selector: cx.new(move |cx| {
let fs = fs.clone();
@@ -48,6 +50,7 @@ impl AgentModelSelector {
}
},
true, // Use popover styles for picker
focus_handle_clone,
window,
cx,
)

View File

@@ -8,9 +8,7 @@ use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore}
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::{
ExternalAgentServerName,
agent_server_store::{
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
},
agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
};
use serde::{Deserialize, Serialize};
use settings::{
@@ -19,6 +17,7 @@ use settings::{
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
use crate::ManageProfiles;
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
use crate::{
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -35,10 +34,7 @@ use crate::{
ExpandMessageEditor,
acp::{AcpThreadHistory, ThreadHistoryEvent},
};
use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
};
use crate::{ManageProfiles, context_store::ContextStore};
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
use anyhow::{Result, anyhow};
@@ -61,7 +57,7 @@ use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
@@ -248,7 +244,6 @@ pub enum AgentType {
Codex,
Custom {
name: SharedString,
command: AgentServerCommand,
},
}
@@ -280,7 +275,7 @@ impl From<ExternalAgent> for AgentType {
ExternalAgent::Gemini => Self::Gemini,
ExternalAgent::ClaudeCode => Self::ClaudeCode,
ExternalAgent::Codex => Self::Codex,
ExternalAgent::Custom { name, command } => Self::Custom { name, command },
ExternalAgent::Custom { name } => Self::Custom { name },
ExternalAgent::NativeAgent => Self::NativeAgent,
}
}
@@ -436,7 +431,6 @@ pub struct AgentPanel {
text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
context_server_registry: Entity<ContextServerRegistry>,
inline_assist_context_store: Entity<ContextStore>,
configuration: Option<Entity<AgentConfiguration>>,
configuration_subscription: Option<Subscription>,
active_view: ActiveView,
@@ -548,7 +542,6 @@ impl AgentPanel {
let client = workspace.client().clone();
let workspace = workspace.weak_handle();
let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
let context_server_registry =
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
@@ -685,7 +678,6 @@ impl AgentPanel {
configuration: None,
configuration_subscription: None,
context_server_registry,
inline_assist_context_store,
previous_view: None,
new_thread_menu_handle: PopoverMenuHandle::default(),
agent_panel_menu_handle: PopoverMenuHandle::default(),
@@ -726,10 +718,6 @@ impl AgentPanel {
&self.prompt_store
}
pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
&self.inline_assist_context_store
}
pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
&self.history_store
}
@@ -828,6 +816,7 @@ impl AgentPanel {
window,
cx,
),
true,
window,
cx,
);
@@ -923,7 +912,12 @@ impl AgentPanel {
)
});
this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
this.set_active_view(
ActiveView::ExternalAgentThread { thread_view },
!loading,
window,
cx,
);
})
})
.detach_and_log_err(cx);
@@ -965,10 +959,10 @@ impl AgentPanel {
fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if matches!(self.active_view, ActiveView::History) {
if let Some(previous_view) = self.previous_view.take() {
self.set_active_view(previous_view, window, cx);
self.set_active_view(previous_view, true, window, cx);
}
} else {
self.set_active_view(ActiveView::History, window, cx);
self.set_active_view(ActiveView::History, true, window, cx);
}
cx.notify();
}
@@ -1024,6 +1018,7 @@ impl AgentPanel {
window,
cx,
),
true,
window,
cx,
);
@@ -1169,7 +1164,7 @@ impl AgentPanel {
let context_server_store = self.project.read(cx).context_server_store();
let fs = self.fs.clone();
self.set_active_view(ActiveView::Configuration, window, cx);
self.set_active_view(ActiveView::Configuration, true, window, cx);
self.configuration = Some(cx.new(|cx| {
AgentConfiguration::new(
fs,
@@ -1286,6 +1281,7 @@ impl AgentPanel {
fn set_active_view(
&mut self,
new_view: ActiveView,
focus: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1324,7 +1320,9 @@ impl AgentPanel {
self.active_view = new_view;
}
self.focus_handle(cx).focus(window);
if focus {
self.focus_handle(cx).focus(window);
}
}
fn populate_recently_opened_menu_section(
@@ -1459,8 +1457,8 @@ impl AgentPanel {
self.serialize(cx);
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
}
AgentType::Custom { name, command } => self.external_thread(
Some(crate::ExternalAgent::Custom { name, command }),
AgentType::Custom { name } => self.external_thread(
Some(crate::ExternalAgent::Custom { name }),
None,
None,
window,
@@ -2085,22 +2083,11 @@ 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.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 {
@@ -2110,7 +2097,6 @@ impl AgentPanel {
.when(
is_agent_selected(AgentType::Custom {
name: agent_name.0.clone(),
command: command.clone(),
}),
|this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
@@ -2121,7 +2107,6 @@ impl AgentPanel {
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let custom_settings = custom_settings.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -2134,17 +2119,6 @@ impl AgentPanel {
name: agent_name
.clone()
.into(),
command: custom_settings
.get(&agent_name.0)
.map(|settings| {
settings
.command
.clone()
})
.unwrap_or(
placeholder_command(
),
),
},
window,
cx,
@@ -2693,23 +2667,19 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
cx: &mut Context<RulesLibrary>,
) {
InlineAssistant::update_global(cx, |assistant, cx| {
let Some(project) = self
.workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().downgrade())
else {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let prompt_store = None;
let thread_store = None;
let context_store = cx.new(|_| ContextStore::new(project.clone()));
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
return;
};
let project = workspace.read(cx).project().downgrade();
assistant.assist(
prompt_editor,
self.workspace.clone(),
context_store,
project,
prompt_store,
thread_store,
panel.read(cx).thread_store().clone(),
None,
initial_prompt,
window,
cx,

View File

@@ -4,14 +4,13 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
mod completion_provider;
mod context;
mod context_picker;
mod context_server_configuration;
mod context_store;
mod context_strip;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
mod mention_set;
mod profile_selector;
mod slash_command;
mod slash_command_picker;
@@ -35,10 +34,9 @@ use language::{
language_settings::{AllLanguageSettings, EditPredictionProvider},
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use project::DisableAiSettings;
use project::agent_server_store::AgentServerCommand;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -57,8 +55,6 @@ actions!(
[
/// Creates a new text-based conversation thread.
NewTextThread,
/// Toggles the context picker interface for adding files, symbols, or other context.
ToggleContextPicker,
/// Toggles the menu to create new agent threads.
ToggleNewThreadMenu,
/// Toggles the navigation menu for switching between threads and views.
@@ -71,8 +67,6 @@ actions!(
ToggleProfileSelector,
/// Cycles through available session modes.
CycleModeSelector,
/// Removes all added context from the current conversation.
RemoveAllContext,
/// Expands the message editor to full size.
ExpandMessageEditor,
/// Opens the conversation history view.
@@ -95,10 +89,6 @@ actions!(
FocusLeft,
/// Moves focus right in the interface.
FocusRight,
/// Removes the currently focused context item.
RemoveFocusedContext,
/// Accepts the suggested context item.
AcceptSuggestedContext,
/// Opens the active thread as a markdown file.
OpenActiveThreadAsMarkdown,
/// Opens the agent diff view to review changes.
@@ -162,18 +152,7 @@ pub enum ExternalAgent {
ClaudeCode,
Codex,
NativeAgent,
Custom {
name: SharedString,
command: AgentServerCommand,
},
}
fn placeholder_command() -> AgentServerCommand {
AgentServerCommand {
path: "/placeholder".into(),
args: vec![],
env: None,
}
Custom { name: SharedString },
}
impl ExternalAgent {
@@ -197,9 +176,7 @@ impl ExternalAgent {
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::Codex => Rc::new(agent_servers::Codex),
Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
Self::Custom { name, command: _ } => {
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
}
Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
}
}
}
@@ -234,11 +211,6 @@ impl ModelUsageContext {
}
}
}
pub fn language_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
self.configured_model(cx)
.map(|configured_model| configured_model.model)
}
}
/// Initializes the `agent` crate.

View File

@@ -1,6 +1,4 @@
use crate::{
context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
};
use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
@@ -8,9 +6,12 @@ use cloud_llm_client::CompletionIntent;
use collections::HashSet;
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
use futures::{
SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
SinkExt, Stream, StreamExt, TryStreamExt as _,
channel::mpsc,
future::{LocalBoxFuture, Shared},
join,
};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
@@ -18,8 +19,7 @@ use language_model::{
};
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::Project;
use prompt_store::{PromptBuilder, PromptStore};
use prompt_store::PromptBuilder;
use rope::Rope;
use smol::future::FutureExt;
use std::{
@@ -43,9 +43,6 @@ pub struct BufferCodegen {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
pub is_insertion: bool,
@@ -56,9 +53,6 @@ impl BufferCodegen {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
initial_transaction_id: Option<TransactionId>,
context_store: Entity<ContextStore>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Arc<Telemetry>,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
@@ -68,9 +62,6 @@ impl BufferCodegen {
buffer.clone(),
range.clone(),
false,
Some(context_store.clone()),
project.clone(),
prompt_store.clone(),
Some(telemetry.clone()),
builder.clone(),
cx,
@@ -85,9 +76,6 @@ impl BufferCodegen {
buffer,
range,
initial_transaction_id,
context_store,
project,
prompt_store,
telemetry,
builder,
};
@@ -148,6 +136,7 @@ impl BufferCodegen {
&mut self,
primary_model: Arc<dyn LanguageModel>,
user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut Context<Self>,
) -> Result<()> {
let alternative_models = LanguageModelRegistry::read_global(cx)
@@ -165,9 +154,6 @@ impl BufferCodegen {
self.buffer.clone(),
self.range.clone(),
false,
Some(self.context_store.clone()),
self.project.clone(),
self.prompt_store.clone(),
Some(self.telemetry.clone()),
self.builder.clone(),
cx,
@@ -180,7 +166,7 @@ impl BufferCodegen {
.zip(&self.alternatives)
{
alternative.update(cx, |alternative, cx| {
alternative.start(user_prompt.clone(), model.clone(), cx)
alternative.start(user_prompt.clone(), context_task.clone(), model.clone(), cx)
})?;
}
@@ -243,9 +229,6 @@ pub struct CodegenAlternative {
status: CodegenStatus,
generation: Task<()>,
diff: Diff,
context_store: Option<Entity<ContextStore>>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>,
_subscription: gpui::Subscription,
builder: Arc<PromptBuilder>,
@@ -264,9 +247,6 @@ impl CodegenAlternative {
buffer: Entity<MultiBuffer>,
range: Range<Anchor>,
active: bool,
context_store: Option<Entity<ContextStore>>,
project: WeakEntity<Project>,
prompt_store: Option<Entity<PromptStore>>,
telemetry: Option<Arc<Telemetry>>,
builder: Arc<PromptBuilder>,
cx: &mut Context<Self>,
@@ -307,9 +287,6 @@ impl CodegenAlternative {
status: CodegenStatus::Idle,
generation: Task::ready(()),
diff: Diff::default(),
context_store,
project,
prompt_store,
telemetry,
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
builder,
@@ -366,6 +343,7 @@ impl CodegenAlternative {
pub fn start(
&mut self,
user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
model: Arc<dyn LanguageModel>,
cx: &mut Context<Self>,
) -> Result<()> {
@@ -384,7 +362,7 @@ impl CodegenAlternative {
if user_prompt.trim().to_lowercase() == "delete" {
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
} else {
let request = self.build_request(&model, user_prompt, cx)?;
let request = self.build_request(&model, user_prompt, context_task, cx)?;
cx.spawn(async move |_, cx| {
Ok(model.stream_completion_text(request.await, cx).await?)
})
@@ -398,6 +376,7 @@ impl CodegenAlternative {
&self,
model: &Arc<dyn LanguageModel>,
user_prompt: String,
context_task: Shared<Task<Option<LoadedContext>>>,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
let buffer = self.buffer.read(cx).snapshot(cx);
@@ -429,22 +408,14 @@ impl CodegenAlternative {
let prompt = self
.builder
.generate_inline_transformation_prompt(user_prompt, language_name, buffer, range)
.generate_inline_transformation_prompt(
user_prompt,
language_name,
buffer,
range.start.0..range.end.0,
)
.context("generating content prompt")?;
let context_task = self.context_store.as_ref().and_then(|context_store| {
if let Some(project) = self.project.upgrade() {
let context = context_store
.read(cx)
.context()
.cloned()
.collect::<Vec<_>>();
Some(load_context(context, &project, &self.prompt_store, cx))
} else {
None
}
});
let temperature = AgentSettings::temperature_for_model(model, cx);
Ok(cx.spawn(async move |_cx| {
@@ -452,12 +423,11 @@ impl CodegenAlternative {
role: Role::User,
content: Vec::new(),
cache: false,
reasoning_details: None,
};
if let Some(context_task) = context_task {
context_task
.await
.add_to_request_message(&mut request_message);
if let Some(context) = context_task.await {
context.add_to_request_message(&mut request_message);
}
request_message.content.push(prompt.into());
@@ -486,6 +456,14 @@ impl CodegenAlternative {
cx: &mut Context<Self>,
) {
let start_time = Instant::now();
// Make a new snapshot and re-resolve anchor in case the document was modified.
// This can happen often if the editor loses focus and is saved + reformatted,
// as in https://github.com/zed-industries/zed/issues/39088
self.snapshot = self.buffer.read(cx).snapshot(cx);
self.range = self.snapshot.anchor_after(self.range.start)
..self.snapshot.anchor_after(self.range.end);
let snapshot = self.snapshot.clone();
let selected_text = snapshot
.text_for_range(self.range.start..self.range.end)
@@ -1075,7 +1053,6 @@ impl Diff {
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use futures::{
Stream,
stream::{self},
@@ -1107,17 +1084,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
)
@@ -1174,17 +1146,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
)
@@ -1243,17 +1210,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
)
@@ -1312,17 +1274,12 @@ mod tests {
snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
true,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
)
@@ -1369,17 +1326,12 @@ mod tests {
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
});
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, vec![], cx).await;
let codegen = cx.new(|cx| {
CodegenAlternative::new(
buffer.clone(),
range.clone(),
false,
None,
project.downgrade(),
None,
None,
prompt_builder,
cx,
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,931 +0,0 @@
mod completion_provider;
pub(crate) mod fetch_context_picker;
pub(crate) mod file_context_picker;
pub(crate) mod rules_context_picker;
pub(crate) mod symbol_context_picker;
pub(crate) mod thread_context_picker;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
use fetch_context_picker::FetchContextPicker;
use file_context_picker::FileContextPicker;
use file_context_picker::render_file_context_entry;
use gpui::{
App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
WeakEntity,
};
use language::Buffer;
use multi_buffer::MultiBufferRow;
use project::ProjectPath;
use prompt_store::PromptStore;
use rules_context_picker::{RulesContextEntry, RulesContextPicker};
use symbol_context_picker::SymbolContextPicker;
use thread_context_picker::render_thread_context_entry;
use ui::{
ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
};
use util::paths::PathStyle;
use util::rel_path::RelPath;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::context_picker::thread_context_picker::ThreadContextPicker;
use crate::{context::RULES_ICON, context_store::ContextStore};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerEntry {
Mode(ContextPickerMode),
Action(ContextPickerAction),
}
impl ContextPickerEntry {
pub fn keyword(&self) -> &'static str {
match self {
Self::Mode(mode) => mode.keyword(),
Self::Action(action) => action.keyword(),
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Mode(mode) => mode.label(),
Self::Action(action) => action.label(),
}
}
pub fn icon(&self) -> IconName {
match self {
Self::Mode(mode) => mode.icon(),
Self::Action(action) => action.icon(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerMode {
File,
Symbol,
Fetch,
Thread,
Rules,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ContextPickerAction {
AddSelections,
}
impl ContextPickerAction {
pub fn keyword(&self) -> &'static str {
match self {
Self::AddSelections => "selection",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::AddSelections => "Selection",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::AddSelections => IconName::Reader,
}
}
}
impl TryFrom<&str> for ContextPickerMode {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"file" => Ok(Self::File),
"symbol" => Ok(Self::Symbol),
"fetch" => Ok(Self::Fetch),
"thread" => Ok(Self::Thread),
"rule" => Ok(Self::Rules),
_ => Err(format!("Invalid context picker mode: {}", value)),
}
}
}
impl ContextPickerMode {
pub fn keyword(&self) -> &'static str {
match self {
Self::File => "file",
Self::Symbol => "symbol",
Self::Fetch => "fetch",
Self::Thread => "thread",
Self::Rules => "rule",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::File => "Files & Directories",
Self::Symbol => "Symbols",
Self::Fetch => "Fetch",
Self::Thread => "Threads",
Self::Rules => "Rules",
}
}
pub fn icon(&self) -> IconName {
match self {
Self::File => IconName::File,
Self::Symbol => IconName::Code,
Self::Fetch => IconName::ToolWeb,
Self::Thread => IconName::Thread,
Self::Rules => RULES_ICON,
}
}
}
#[derive(Debug, Clone)]
enum ContextPickerState {
Default(Entity<ContextMenu>),
File(Entity<FileContextPicker>),
Symbol(Entity<SymbolContextPicker>),
Fetch(Entity<FetchContextPicker>),
Thread(Entity<ThreadContextPicker>),
Rules(Entity<RulesContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerState,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
_subscriptions: Vec<Subscription>,
}
impl ContextPicker {
pub fn new(
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = context_store
.upgrade()
.map(|context_store| {
cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
})
.into_iter()
.chain(
thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
.map(|thread_store| {
cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
}),
)
.collect::<Vec<Subscription>>();
ContextPicker {
mode: ContextPickerState::Default(ContextMenu::build(
window,
cx,
|menu, _window, _cx| menu,
)),
workspace,
context_store,
thread_store,
prompt_store,
_subscriptions: subscriptions,
}
}
pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = ContextPickerState::Default(self.build_menu(window, cx));
cx.notify();
}
fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
let context_picker = cx.entity();
let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
let Some(workspace) = self.workspace.upgrade() else {
return menu;
};
let path_style = workspace.read(cx).path_style(cx);
let recent = self.recent_entries(cx);
let has_recent = !recent.is_empty();
let recent_entries = recent
.into_iter()
.enumerate()
.map(|(ix, entry)| {
self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
})
.collect::<Vec<_>>();
let entries = self
.workspace
.upgrade()
.map(|workspace| {
available_context_picker_entries(
&self.prompt_store,
&self.thread_store,
&workspace,
cx,
)
})
.unwrap_or_default();
menu.when(has_recent, |menu| {
menu.custom_row(|_, _| {
div()
.mb_1()
.child(
Label::new("Recent")
.color(Color::Muted)
.size(LabelSize::Small),
)
.into_any_element()
})
})
.extend(recent_entries)
.when(has_recent, |menu| menu.separator())
.extend(entries.into_iter().map(|entry| {
let context_picker = context_picker.clone();
ContextMenuEntry::new(entry.label())
.icon(entry.icon())
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.handler(move |window, cx| {
context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
})
}))
.keep_open_on_confirm(true)
});
cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
cx.emit(DismissEvent);
})
.detach();
menu
}
/// Whether threads are allowed as context.
pub fn allow_threads(&self) -> bool {
self.thread_store.is_some()
}
fn select_entry(
&mut self,
entry: ContextPickerEntry,
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_picker = cx.entity().downgrade();
match entry {
ContextPickerEntry::Mode(mode) => match mode {
ContextPickerMode::File => {
self.mode = ContextPickerState::File(cx.new(|cx| {
FileContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Symbol => {
self.mode = ContextPickerState::Symbol(cx.new(|cx| {
SymbolContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Rules => {
if let Some(prompt_store) = self.prompt_store.as_ref() {
self.mode = ContextPickerState::Rules(cx.new(|cx| {
RulesContextPicker::new(
prompt_store.clone(),
context_picker.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
}
ContextPickerMode::Fetch => {
self.mode = ContextPickerState::Fetch(cx.new(|cx| {
FetchContextPicker::new(
context_picker.clone(),
self.workspace.clone(),
self.context_store.clone(),
window,
cx,
)
}));
}
ContextPickerMode::Thread => {
if let Some(thread_store) = self.thread_store.clone() {
self.mode = ContextPickerState::Thread(cx.new(|cx| {
ThreadContextPicker::new(
thread_store,
context_picker.clone(),
self.context_store.clone(),
self.workspace.clone(),
window,
cx,
)
}));
}
}
},
ContextPickerEntry::Action(action) => match action {
ContextPickerAction::AddSelections => {
if let Some((context_store, workspace)) =
self.context_store.upgrade().zip(self.workspace.upgrade())
{
add_selections_as_context(&context_store, &workspace, cx);
}
cx.emit(DismissEvent);
}
},
}
cx.notify();
cx.focus_self(window);
}
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Other variants already select their first entry on open automatically
if let ContextPickerState::Default(entity) = &self.mode {
entity.update(cx, |entity, cx| {
entity.select_first(&Default::default(), window, cx)
})
}
}
fn recent_menu_item(
&self,
context_picker: Entity<ContextPicker>,
ix: usize,
entry: RecentEntry,
path_style: PathStyle,
) -> ContextMenuItem {
match entry {
RecentEntry::File {
project_path,
path_prefix,
} => {
let context_store = self.context_store.clone();
let worktree_id = project_path.worktree_id;
let path = project_path.path.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_file_context_entry(
ElementId::named_usize("ctx-recent", ix),
worktree_id,
&path,
&path_prefix,
false,
path_style,
context_store.clone(),
cx,
)
.into_any()
},
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_file(project_path.clone(), window, cx);
})
},
None,
)
}
RecentEntry::Thread(thread) => {
let context_store = self.context_store.clone();
let view_thread = thread.clone();
ContextMenuItem::custom_entry(
move |_window, cx| {
render_thread_context_entry(&view_thread, context_store.clone(), cx)
.into_any()
},
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_thread(thread.clone(), window, cx)
.detach_and_log_err(cx);
})
},
None,
)
}
}
}
fn add_recent_file(
&self,
project_path: ProjectPath,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(context_store) = self.context_store.upgrade() else {
return;
};
let task = context_store.update(cx, |context_store, cx| {
context_store.add_file_from_path(project_path.clone(), true, cx)
});
cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
.detach();
cx.notify();
}
fn add_recent_thread(
&self,
entry: HistoryEntry,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(context_store) = self.context_store.upgrade() else {
return Task::ready(Err(anyhow!("context store not available")));
};
let Some(project) = self
.workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return Task::ready(Err(anyhow!("project not available")));
};
match entry {
HistoryEntry::AcpThread(thread) => {
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return Task::ready(Err(anyhow!("thread store not available")));
};
let load_thread_task =
agent::load_agent_thread(thread.id, thread_store, project, cx);
cx.spawn(async move |this, cx| {
let thread = load_thread_task.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx);
})?;
this.update(cx, |_this, cx| cx.notify())
})
}
HistoryEntry::TextThread(thread) => {
let Some(thread_store) = self
.thread_store
.as_ref()
.and_then(|thread_store| thread_store.upgrade())
else {
return Task::ready(Err(anyhow!("text thread store not available")));
};
let task = thread_store.update(cx, |this, cx| {
this.load_text_thread(thread.path.clone(), cx)
});
cx.spawn(async move |this, cx| {
let thread = task.await?;
context_store.update(cx, |context_store, cx| {
context_store.add_text_thread(thread, true, cx);
})?;
this.update(cx, |_this, cx| cx.notify())
})
}
}
}
fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
let Some(workspace) = self.workspace.upgrade() else {
return vec![];
};
let Some(context_store) = self.context_store.upgrade() else {
return vec![];
};
recent_context_picker_entries_with_store(
context_store,
self.thread_store.clone(),
workspace,
None,
cx,
)
}
fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
match &self.mode {
ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
}
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
impl Focusable for ContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
ContextPickerState::Default(menu) => menu.focus_handle(cx),
ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
ContextPickerState::Default(menu) => parent.child(menu.clone()),
ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
ContextPickerState::Rules(user_rules_picker) => {
parent.child(user_rules_picker.clone())
}
})
}
}
pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<RelPath>,
},
Thread(HistoryEntry),
}
pub(crate) fn available_context_picker_entries(
prompt_store: &Option<WeakEntity<PromptStore>>,
thread_store: &Option<WeakEntity<HistoryStore>>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<ContextPickerEntry> {
let mut entries = vec![
ContextPickerEntry::Mode(ContextPickerMode::File),
ContextPickerEntry::Mode(ContextPickerMode::Symbol),
];
let has_selection = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.downcast::<Editor>())
.is_some_and(|editor| {
editor.update(cx, |editor, cx| {
editor.has_non_empty_selection(&editor.display_snapshot(cx))
})
});
if has_selection {
entries.push(ContextPickerEntry::Action(
ContextPickerAction::AddSelections,
));
}
if thread_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
}
if prompt_store.is_some() {
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
}
entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
entries
}
fn recent_context_picker_entries_with_store(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<HistoryStore>>,
workspace: Entity<Workspace>,
exclude_path: Option<ProjectPath>,
cx: &App,
) -> Vec<RecentEntry> {
let project = workspace.read(cx).project();
let mut exclude_paths = context_store.read(cx).file_paths(cx);
exclude_paths.extend(exclude_path);
let exclude_paths = exclude_paths
.into_iter()
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
.collect();
let exclude_threads = context_store.read(cx).thread_ids();
recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
}
pub(crate) fn recent_context_picker_entries(
thread_store: Option<WeakEntity<HistoryStore>>,
workspace: Entity<Workspace>,
exclude_paths: &HashSet<PathBuf>,
exclude_threads: &HashSet<acp::SessionId>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let include_root_name = workspace.visible_worktrees(cx).count() > 1;
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(_, abs_path)| {
abs_path
.as_ref()
.is_none_or(|path| !exclude_paths.contains(path.as_path()))
})
.take(4)
.filter_map(|(project_path, _)| {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|worktree| {
let path_prefix = if include_root_name {
worktree.read(cx).root_name().into()
} else {
RelPath::empty().into()
};
RecentEntry::File {
project_path,
path_prefix,
}
})
}),
);
if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
const RECENT_THREADS_COUNT: usize = 2;
recent.extend(
thread_store
.read(cx)
.recently_opened_entries(cx)
.iter()
.filter(|e| match e.id() {
HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
HistoryEntryId::TextThread(path) => {
!exclude_paths.contains(&path.to_path_buf())
}
})
.take(RECENT_THREADS_COUNT)
.map(|thread| RecentEntry::Thread(thread.clone())),
);
}
recent
}
fn add_selections_as_context(
context_store: &Entity<ContextStore>,
workspace: &Entity<Workspace>,
cx: &mut App,
) {
let selection_ranges = selection_ranges(workspace, cx);
context_store.update(cx, |context_store, cx| {
for (buffer, range) in selection_ranges {
context_store.add_selection(buffer, range, cx);
}
})
}
pub(crate) fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
let Some(editor) = workspace
.read(cx)
.active_item(cx)
.and_then(|item| item.act_as::<Editor>(cx))
else {
return Vec::new();
};
editor.update(cx, |editor, cx| {
let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
let buffer = editor.buffer().clone().read(cx);
let snapshot = buffer.snapshot(cx);
selections
.into_iter()
.map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
.flat_map(|range| {
let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
if start_buffer != end_buffer {
return None;
}
Some((start_buffer, start..end))
})
.collect::<Vec<_>>()
})
}
pub(crate) fn insert_crease_for_mention(
excerpt_id: ExcerptId,
crease_start: text::Anchor,
content_len: usize,
crease_label: SharedString,
crease_icon_path: SharedString,
editor_entity: Entity<Editor>,
window: &mut Window,
cx: &mut App,
) -> Option<CreaseId> {
editor_entity.update(cx, |editor, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
let start = start.bias_right(&snapshot);
let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
let crease = crease_for_mention(
crease_label,
crease_icon_path,
start..end,
editor_entity.downgrade(),
);
let ids = editor.insert_creases(vec![crease.clone()], cx);
editor.fold_creases(vec![crease], false, window, cx);
Some(ids[0])
})
}
pub fn crease_for_mention(
label: SharedString,
icon_path: SharedString,
range: Range<Anchor>,
editor_entity: WeakEntity<Editor>,
) -> Crease<Anchor> {
let placeholder = FoldPlaceholder {
render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
merge_adjacent: false,
..Default::default()
};
let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
.with_metadata(CreaseMetadata { icon_path, label })
}
fn render_fold_icon_button(
icon_path: SharedString,
label: SharedString,
editor: WeakEntity<Editor>,
) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
Arc::new({
move |fold_id, fold_range, cx| {
let is_in_text_selection = editor
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
.unwrap_or_default();
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon_path.clone())
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
),
)
.into_any_element()
}
})
}
fn fold_toggle(
name: &'static str,
) -> impl Fn(
MultiBufferRow,
bool,
Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
&mut Window,
&mut App,
) -> AnyElement {
move |row, is_folded, fold, _window, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.on_click(move |_e, window, cx| fold(!is_folded, window, cx))
.into_any_element()
}
}
pub struct MentionLink;
impl MentionLink {
const FILE: &str = "@file";
const SYMBOL: &str = "@symbol";
const SELECTION: &str = "@selection";
const THREAD: &str = "@thread";
const FETCH: &str = "@fetch";
const RULE: &str = "@rule";
const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
pub fn for_file(file_name: &str, full_path: &str) -> String {
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
}
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
format!(
"[@{}]({}:{}:{})",
symbol_name,
Self::SYMBOL,
full_path,
symbol_name
)
}
pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
line_range.start + 1,
line_range.end + 1,
Self::SELECTION,
full_path,
line_range.start,
line_range.end
)
}
pub fn for_thread(thread: &HistoryEntry) -> String {
match thread {
HistoryEntry::AcpThread(thread) => {
format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
}
HistoryEntry::TextThread(thread) => {
let filename = thread
.path
.file_name()
.unwrap_or_default()
.to_string_lossy();
let escaped_filename = urlencoding::encode(&filename);
format!(
"[@{}]({}:{}{})",
thread.title,
Self::THREAD,
Self::TEXT_THREAD_URL_PREFIX,
escaped_filename
)
}
}
}
pub fn for_fetch(url: &str) -> String {
format!("[@{}]({}:{})", url, Self::FETCH, url)
}
pub fn for_rule(rule: &RulesContextEntry) -> String {
format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,252 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{Context as _, Result, bail};
use futures::AsyncReadExt as _;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{Context, ListItem, Window, prelude::*};
use workspace::Workspace;
use crate::{context_picker::ContextPicker, context_store::ContextStore};
pub struct FetchContextPicker {
picker: Entity<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for FetchContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub struct FetchContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
context_store,
url: String::new(),
}
}
}
pub(crate) async fn fetch_url_content(
http_client: Arc<HttpClientWithUrl>,
url: String,
) -> Result<String> {
let url = if !url.starts_with("https://") && !url.starts_with("http://") {
format!("https://{url}")
} else {
url
};
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
if self.url.is_empty() { 0 } else { 1 }
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some("Enter the URL that you would like to fetch".into())
}
fn selected_index(&self) -> usize {
0
}
fn set_selected_index(
&mut self,
_ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client();
let url = self.url.clone();
cx.spawn_in(window, async move |this, cx| {
let text = cx
.background_spawn(fetch_url_content(http_client, url.clone()))
.await?;
this.update(cx, |this, cx| {
this.delegate.context_store.update(cx, |context_store, cx| {
context_store.add_fetched_url(url, text, cx)
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let added = self
.context_store
.upgrade()
.is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(Label::new(self.url.clone()))
.when(added, |child| {
child.disabled(true).end_slot(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
}),
)
}
}

View File

@@ -1,392 +0,0 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use file_icons::FileIcons;
use fuzzy::PathMatch;
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
use ui::{ListItem, Tooltip, prelude::*};
use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
use workspace::Workspace;
use crate::{
context_picker::ContextPicker,
context_store::{ContextStore, FileInclusion},
};
pub struct FileContextPicker {
picker: Entity<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for FileContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
matches: Vec<FileMatch>,
selected_index: usize,
}
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for FileContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search files & directories…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn_in(window, async move |this, cx| {
// TODO: This should be probably be run in the background.
let paths = search_task.await;
this.update(cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
return;
};
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
let is_directory = mat.is_dir;
self.context_store
.update(cx, |context_store, cx| {
if is_directory {
context_store
.add_directory(&project_path, true, cx)
.log_err();
} else {
context_store
.add_file_from_path(project_path.clone(), true, cx)
.detach_and_log_err(cx);
}
})
.ok();
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let FileMatch { mat, .. } = &self.matches.get(ix)?;
let workspace = self.workspace.upgrade()?;
let path_style = workspace.read(cx).path_style(cx);
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(render_file_context_entry(
ElementId::named_usize("file-ctx-picker", ix),
WorktreeId::from_usize(mat.worktree_id),
&mat.path,
&mat.path_prefix,
mat.is_dir,
path_style,
self.context_store.clone(),
cx,
)),
)
}
}
pub struct FileMatch {
pub mat: PathMatch,
pub is_recent: bool,
}
pub(crate) fn search_files(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &App,
) -> Task<Vec<FileMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
let include_root_name = visible_worktrees.len() > 1;
let recent_matches = workspace
.recent_navigation_history(Some(10), cx)
.into_iter()
.map(|(project_path, _)| {
let path_prefix = if include_root_name {
project
.worktree_for_id(project_path.worktree_id, cx)
.map(|wt| wt.read(cx).root_name().into())
.unwrap_or_else(|| RelPath::empty().into())
} else {
RelPath::empty().into()
};
FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: project_path.worktree_id.to_usize(),
path: project_path.path,
path_prefix,
distance_to_relative_ancestor: 0,
is_dir: false,
},
is_recent: true,
}
});
let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
let worktree = worktree.read(cx);
let path_prefix: Arc<RelPath> = if include_root_name {
worktree.root_name().into()
} else {
RelPath::empty().into()
};
worktree.entries(false, 0).map(move |entry| FileMatch {
mat: PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: worktree.id().to_usize(),
path: entry.path.clone(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: entry.is_dir(),
},
is_recent: false,
})
});
Task::ready(recent_matches.chain(file_matches).collect())
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let include_root_name = worktrees.len() > 1;
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
include_root_name,
candidates: project::Candidates::Entries,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
&None,
false,
100,
&cancellation_flag,
executor,
)
.await
.into_iter()
.map(|mat| FileMatch {
mat,
is_recent: false,
})
.collect::<Vec<_>>()
})
}
}
pub fn extract_file_name_and_directory(
path: &RelPath,
path_prefix: &RelPath,
path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
// If path is empty, this means we're matching with the root directory itself
// so we use the path_prefix as the name
if path.is_empty() && !path_prefix.is_empty() {
return (path_prefix.display(path_style).to_string().into(), None);
}
let full_path = path_prefix.join(path);
let file_name = full_path.file_name().unwrap_or_default();
let display_path = full_path.display(path_style);
let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
(
file_name.to_string().into(),
Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
)
}
pub fn render_file_context_entry(
id: ElementId,
worktree_id: WorktreeId,
path: &Arc<RelPath>,
path_prefix: &Arc<RelPath>,
is_directory: bool,
path_style: PathStyle,
context_store: WeakEntity<ContextStore>,
cx: &App,
) -> Stateful<Div> {
let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
let added = context_store.upgrade().and_then(|context_store| {
let project_path = ProjectPath {
worktree_id,
path: path.clone(),
};
if is_directory {
context_store
.read(cx)
.path_included_in_directory(&project_path, cx)
} else {
context_store.read(cx).file_path_included(&project_path, cx)
}
});
let file_icon = if is_directory {
FileIcons::get_folder_icon(false, path.as_std_path(), cx)
} else {
FileIcons::get_icon(path.as_std_path(), cx)
}
.map(Icon::from_path)
.unwrap_or_else(|| Icon::new(IconName::File));
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(file_icon.size(IconSize::Small).color(Color::Muted))
.child(
h_flex()
.gap_1()
.child(Label::new(file_name))
.children(directory.map(|directory| {
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
)
.when_some(added, |el, added| match added {
FileInclusion::Direct => el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
),
FileInclusion::InDirectory { full_path } => {
let directory_full_path = full_path.to_string_lossy().into_owned();
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Included").size(LabelSize::Small)),
)
.tooltip(Tooltip::text(format!("in {directory_full_path}")))
}
})
}

View File

@@ -1,224 +0,0 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use prompt_store::{PromptId, PromptStore, UserPromptId};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use crate::{
context::RULES_ICON,
context_picker::ContextPicker,
context_store::{self, ContextStore},
};
pub struct RulesContextPicker {
picker: Entity<Picker<RulesContextPickerDelegate>>,
}
impl RulesContextPicker {
pub fn new(
prompt_store: WeakEntity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
RulesContextPicker { picker }
}
}
impl Focusable for RulesContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for RulesContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, Clone)]
pub struct RulesContextEntry {
pub prompt_id: UserPromptId,
pub title: SharedString,
}
pub struct RulesContextPickerDelegate {
prompt_store: WeakEntity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
matches: Vec<RulesContextEntry>,
selected_index: usize,
}
impl RulesContextPickerDelegate {
pub fn new(
prompt_store: WeakEntity<PromptStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
) -> Self {
RulesContextPickerDelegate {
prompt_store,
context_picker,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for RulesContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search available rules…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(prompt_store) = self.prompt_store.upgrade() else {
return Task::ready(());
};
let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(entry) = self.matches.get(self.selected_index) else {
return;
};
self.context_store
.update(cx, |context_store, cx| {
context_store.add_rules(entry.prompt_id, true, cx)
})
.log_err();
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
user_rules: &RulesContextEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
let added = context_store.upgrade().is_some_and(|context_store| {
context_store
.read(cx)
.includes_user_rules(user_rules.prompt_id)
});
h_flex()
.gap_1p5()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1p5()
.max_w_72()
.child(
Icon::new(RULES_ICON)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(user_rules.title.clone()).truncate()),
)
.when(added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}
pub(crate) fn search_rules(
query: String,
cancellation_flag: Arc<AtomicBool>,
prompt_store: &Entity<PromptStore>,
cx: &mut App,
) -> Task<Vec<RulesContextEntry>> {
let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
cx.background_spawn(async move {
search_task
.await
.into_iter()
.flat_map(|metadata| {
// Default prompts are filtered out as they are automatically included.
if metadata.default {
None
} else {
match metadata.id {
PromptId::EditWorkflow => None,
PromptId::User { uuid } => Some(RulesContextEntry {
prompt_id: uuid,
title: metadata.title?,
}),
}
}
})
.collect::<Vec<_>>()
})
}

View File

@@ -1,415 +0,0 @@
use std::cmp::Reverse;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use anyhow::{Result, anyhow};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use project::lsp_store::SymbolLocation;
use project::{DocumentSymbol, Symbol};
use ui::{ListItem, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use crate::{
context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
};
pub struct SymbolContextPicker {
picker: Entity<Picker<SymbolContextPickerDelegate>>,
}
impl SymbolContextPicker {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Focusable for SymbolContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SymbolContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct SymbolContextPickerDelegate {
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
matches: Vec<SymbolEntry>,
selected_index: usize,
}
impl SymbolContextPickerDelegate {
pub fn new(
context_picker: WeakEntity<ContextPicker>,
workspace: WeakEntity<Workspace>,
context_store: WeakEntity<ContextStore>,
) -> Self {
Self {
context_picker,
workspace,
context_store,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for SymbolContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search symbols…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
let context_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let symbols = search_task.await;
let symbol_entries = context_store
.read_with(cx, |context_store, cx| {
compute_symbol_entries(symbols, context_store, cx)
})
.log_err()
.unwrap_or_default();
this.update(cx, |this, _cx| {
this.delegate.matches = symbol_entries;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(mat) = self.matches.get(self.selected_index) else {
return;
};
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let add_symbol_task = add_symbol(
mat.symbol.clone(),
true,
workspace,
self.context_store.clone(),
cx,
);
let selected_index = self.selected_index;
cx.spawn(async move |this, cx| {
let (_, included) = add_symbol_task.await?;
this.update(cx, |this, _| {
if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
mat.is_included = included;
}
})
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
))
}
}
pub(crate) struct SymbolEntry {
pub symbol: Symbol,
pub is_included: bool,
}
pub(crate) fn add_symbol(
symbol: Symbol,
remove_if_exists: bool,
workspace: Entity<Workspace>,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
let project = workspace.read(cx).project().clone();
let open_buffer_task = project.update(cx, |project, cx| {
let SymbolLocation::InProject(symbol_path) = &symbol.path else {
return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
};
project.open_buffer(symbol_path.clone(), cx)
});
cx.spawn(async move |cx| {
let buffer = open_buffer_task.await?;
let document_symbols = project
.update(cx, |project, cx| project.document_symbols(&buffer, cx))?
.await?;
// Try to find a matching document symbol. Document symbols include
// not only the symbol itself (e.g. function name), but they also
// include the context that they contain (e.g. function body).
let (name, range, enclosing_range) = if let Some(DocumentSymbol {
name,
range,
selection_range,
..
}) =
find_matching_symbol(&symbol, document_symbols.as_slice())
{
(name, selection_range, range)
} else {
// If we do not find a matching document symbol, fall back to
// just the symbol itself
(symbol.name, symbol.range.clone(), symbol.range)
};
let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
(
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
buffer.anchor_after(enclosing_range.start)
..buffer.anchor_before(enclosing_range.end),
)
})?;
context_store.update(cx, move |context_store, cx| {
context_store.add_symbol(
buffer,
name.into(),
range,
enclosing_range,
remove_if_exists,
cx,
)
})
})
}
fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
let mut candidates = candidates.iter();
let mut candidate = candidates.next()?;
loop {
if candidate.range.start > symbol.range.end {
return None;
}
if candidate.range.end < symbol.range.start {
candidate = candidates.next()?;
continue;
}
if candidate.selection_range == symbol.range {
return Some(candidate.clone());
}
if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
candidates = candidate.children.iter();
candidate = candidates.next()?;
continue;
}
return None;
}
}
pub struct SymbolMatch {
pub symbol: Symbol,
}
pub(crate) fn search_symbols(
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Task<Vec<SymbolMatch>> {
let symbols_task = workspace.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.symbols(&query, cx))
});
let project = workspace.read(cx).project().clone();
cx.spawn(async move |cx| {
let Some(symbols) = symbols_task.await.log_err() else {
return Vec::new();
};
let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
project
.update(cx, |project, cx| {
symbols
.iter()
.enumerate()
.map(|(id, symbol)| {
StringMatchCandidate::new(id, symbol.label.filter_text())
})
.partition(|candidate| match &symbols[candidate.id].path {
SymbolLocation::InProject(project_path) => project
.entry_for_path(project_path, cx)
.is_some_and(|e| !e.is_ignored),
SymbolLocation::OutsideProject { .. } => false,
})
})
.log_err()
else {
return Vec::new();
};
const MAX_MATCHES: usize = 100;
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
&visible_match_candidates,
&query,
false,
true,
MAX_MATCHES,
&cancellation_flag,
cx.background_executor().clone(),
));
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
&external_match_candidates,
&query,
false,
true,
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
&cancellation_flag,
cx.background_executor().clone(),
));
let sort_key_for_match = |mat: &StringMatch| {
let symbol = &symbols[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
};
visible_matches.sort_unstable_by_key(sort_key_for_match);
external_matches.sort_unstable_by_key(sort_key_for_match);
let mut matches = visible_matches;
matches.append(&mut external_matches);
matches
.into_iter()
.map(|mut mat| {
let symbol = symbols[mat.candidate_id].clone();
let filter_start = symbol.label.filter_range.start;
for position in &mut mat.positions {
*position += filter_start;
}
SymbolMatch { symbol }
})
.collect()
})
}
fn compute_symbol_entries(
symbols: Vec<SymbolMatch>,
context_store: &ContextStore,
cx: &App,
) -> Vec<SymbolEntry> {
symbols
.into_iter()
.map(|SymbolMatch { symbol, .. }| SymbolEntry {
is_included: context_store.includes_symbol(&symbol, cx),
symbol,
})
.collect::<Vec<_>>()
}
pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
let path = match &entry.symbol.path {
SymbolLocation::InProject(project_path) => {
project_path.path.file_name().unwrap_or_default().into()
}
SymbolLocation::OutsideProject {
abs_path,
signature: _,
} => abs_path
.file_name()
.map(|f| f.to_string_lossy())
.unwrap_or_default(),
};
let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
h_flex()
.id(id)
.gap_1p5()
.w_full()
.child(
Icon::new(IconName::Code)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
.gap_1()
.child(Label::new(&entry.symbol.name))
.child(
Label::new(symbol_location)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when(entry.is_included, |el| {
el.child(
h_flex()
.w_full()
.justify_end()
.gap_0p5()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}

View File

@@ -1,280 +0,0 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::{
context_picker::ContextPicker,
context_store::{self, ContextStore},
};
use agent::{HistoryEntry, HistoryStore};
use fuzzy::StringMatchCandidate;
use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
use picker::{Picker, PickerDelegate};
use ui::{ListItem, prelude::*};
use workspace::Workspace;
pub struct ThreadContextPicker {
picker: Entity<Picker<ThreadContextPickerDelegate>>,
}
impl ThreadContextPicker {
pub fn new(
thread_store: WeakEntity<HistoryStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = ThreadContextPickerDelegate::new(
thread_store,
context_picker,
context_store,
workspace,
);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
ThreadContextPicker { picker }
}
}
impl Focusable for ThreadContextPicker {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ThreadContextPicker {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct ThreadContextPickerDelegate {
thread_store: WeakEntity<HistoryStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
workspace: WeakEntity<Workspace>,
matches: Vec<HistoryEntry>,
selected_index: usize,
}
impl ThreadContextPickerDelegate {
pub fn new(
thread_store: WeakEntity<HistoryStore>,
context_picker: WeakEntity<ContextPicker>,
context_store: WeakEntity<context_store::ContextStore>,
workspace: WeakEntity<Workspace>,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_store,
workspace,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ThreadContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search threads…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let Some(thread_store) = self.thread_store.upgrade() else {
return Task::ready(());
};
let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
cx.spawn_in(window, async move |this, cx| {
let matches = search_task.await;
this.update(cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(project) = self
.workspace
.upgrade()
.map(|w| w.read(cx).project().clone())
else {
return;
};
let Some((entry, thread_store)) = self
.matches
.get(self.selected_index)
.zip(self.thread_store.upgrade())
else {
return;
};
match entry {
HistoryEntry::AcpThread(thread) => {
let load_thread_task =
agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
cx.spawn(async move |this, cx| {
let thread = load_thread_task.await?;
this.update(cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_thread(thread, true, cx)
})
.ok();
})
})
.detach_and_log_err(cx);
}
HistoryEntry::TextThread(thread) => {
let task = thread_store.update(cx, |this, cx| {
this.load_text_thread(thread.path.clone(), cx)
});
cx.spawn(async move |this, cx| {
let thread = task.await?;
this.update(cx, |this, cx| {
this.delegate
.context_store
.update(cx, |context_store, cx| {
context_store.add_text_thread(thread, true, cx)
})
.ok();
})
})
.detach_and_log_err(cx);
}
}
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.context_picker
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches.get(ix)?;
Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
render_thread_context_entry(thread, self.context_store.clone(), cx),
))
}
}
pub fn render_thread_context_entry(
entry: &HistoryEntry,
context_store: WeakEntity<ContextStore>,
cx: &mut App,
) -> Div {
let is_added = match entry {
HistoryEntry::AcpThread(thread) => context_store
.upgrade()
.is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
HistoryEntry::TextThread(thread) => context_store
.upgrade()
.is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
};
h_flex()
.gap_1p5()
.w_full()
.justify_between()
.child(
h_flex()
.gap_1p5()
.max_w_72()
.child(
Icon::new(IconName::Thread)
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(entry.title().clone()).truncate()),
)
.when(is_added, |el| {
el.child(
h_flex()
.gap_1()
.child(
Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Success),
)
.child(Label::new("Added").size(LabelSize::Small)),
)
})
}
pub(crate) fn search_threads(
query: String,
cancellation_flag: Arc<AtomicBool>,
thread_store: &Entity<HistoryStore>,
cx: &mut App,
) -> Task<Vec<HistoryEntry>> {
let threads = thread_store.read(cx).entries().collect();
if query.is_empty() {
return Task::ready(threads);
}
let executor = cx.background_executor().clone();
cx.background_spawn(async move {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
true,
100,
&cancellation_flag,
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
})
}

View File

@@ -1,614 +0,0 @@
use crate::context::{
AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
use agent_client_protocol as acp;
use anyhow::{Context as _, Result, anyhow};
use assistant_text_thread::TextThread;
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::{Buffer, File as _};
use language_model::LanguageModelImage;
use project::{
Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
lsp_store::SymbolLocation,
};
use prompt_store::UserPromptId;
use ref_cast::RefCast as _;
use std::{
ops::Range,
path::{Path, PathBuf},
sync::Arc,
};
use text::{Anchor, OffsetRangeExt};
pub struct ContextStore {
project: WeakEntity<Project>,
next_context_id: ContextId,
context_set: IndexSet<AgentContextKey>,
context_thread_ids: HashSet<acp::SessionId>,
context_text_thread_paths: HashSet<Arc<Path>>,
}
pub enum ContextStoreEvent {
ContextRemoved(AgentContextKey),
}
impl EventEmitter<ContextStoreEvent> for ContextStore {}
impl ContextStore {
pub fn new(project: WeakEntity<Project>) -> Self {
Self {
project,
next_context_id: ContextId::zero(),
context_set: IndexSet::default(),
context_thread_ids: HashSet::default(),
context_text_thread_paths: HashSet::default(),
}
}
pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
self.context_set.iter().map(|entry| entry.as_ref())
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.context_set.clear();
self.context_thread_ids.clear();
cx.notify();
}
pub fn add_file_from_path(
&mut self,
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<Option<AgentContextHandle>>> {
let Some(project) = self.project.upgrade() else {
return Task::ready(Err(anyhow!("failed to read project")));
};
if is_image_file(&project, &project_path, cx) {
self.add_image_from_path(project_path, remove_if_exists, cx)
} else {
cx.spawn(async move |this, cx| {
let open_buffer_task = project.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?;
let buffer = open_buffer_task.await?;
this.update(cx, |this, cx| {
this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
})
})
}
}
pub fn add_file_from_buffer(
&mut self,
project_path: &ProjectPath,
buffer: Entity<Buffer>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(key.as_ref().clone())
}
} else if self.path_included_in_directory(project_path, cx).is_some() {
None
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_directory(
&mut self,
project_path: &ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Result<Option<AgentContextHandle>> {
let project = self.project.upgrade().context("failed to read project")?;
let entry_id = project
.read(cx)
.entry_for_path(project_path, cx)
.map(|entry| entry.id)
.context("no entry found for directory context")?;
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Directory(DirectoryContextHandle {
entry_id,
context_id,
});
let context =
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
};
anyhow::Ok(context)
}
pub fn add_symbol(
&mut self,
buffer: Entity<Buffer>,
symbol: SharedString,
range: Range<Anchor>,
enclosing_range: Range<Anchor>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> (Option<AgentContextHandle>, bool) {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Symbol(SymbolContextHandle {
buffer,
symbol,
range,
enclosing_range,
context_id,
});
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
let handle = if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(key.as_ref().clone())
};
return (handle, false);
}
let included = self.insert_context(context.clone(), cx);
(Some(context), included)
}
pub fn add_thread(
&mut self,
thread: Entity<agent::Thread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_text_thread(
&mut self,
text_thread: Entity<TextThread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::TextThread(TextThreadContextHandle {
text_thread,
context_id,
});
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_rules(
&mut self,
prompt_id: UserPromptId,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Rules(RulesContextHandle {
prompt_id,
context_id,
});
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
}
}
pub fn add_fetched_url(
&mut self,
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) -> AgentContextHandle {
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
url: url.into(),
text: text.into(),
context_id: self.next_context_id.post_inc(),
});
self.insert_context(context.clone(), cx);
context
}
pub fn add_image_from_path(
&mut self,
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Task<Result<Option<AgentContextHandle>>> {
let project = self.project.clone();
cx.spawn(async move |this, cx| {
let open_image_task = project.update(cx, |project, cx| {
project.open_image(project_path.clone(), cx)
})?;
let image_item = open_image_task.await?;
this.update(cx, |this, cx| {
let item = image_item.read(cx);
this.insert_image(
Some(item.project_path(cx)),
Some(item.file.full_path(cx).to_string_lossy().into_owned()),
item.image.clone(),
remove_if_exists,
cx,
)
})
})
}
pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
self.insert_image(None, None, image, false, cx);
}
fn insert_image(
&mut self,
project_path: Option<ProjectPath>,
full_path: Option<String>,
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Option<AgentContextHandle> {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContextHandle::Image(ImageContext {
project_path,
full_path,
original_image: image,
image_task,
context_id: self.next_context_id.post_inc(),
});
if self.has_context(&context) && remove_if_exists {
self.remove_context(&context, cx);
return None;
}
self.insert_context(context.clone(), cx);
Some(context)
}
pub fn add_selection(
&mut self,
buffer: Entity<Buffer>,
range: Range<Anchor>,
cx: &mut Context<ContextStore>,
) {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Selection(SelectionContextHandle {
buffer,
range,
context_id,
});
self.insert_context(context, cx);
}
pub fn add_suggested_context(
&mut self,
suggested: &SuggestedContext,
cx: &mut Context<ContextStore>,
) {
match suggested {
SuggestedContext::File {
buffer,
icon_path: _,
name: _,
} => {
if let Some(buffer) = buffer.upgrade() {
let context_id = self.next_context_id.post_inc();
self.insert_context(
AgentContextHandle::File(FileContextHandle { buffer, context_id }),
cx,
);
};
}
SuggestedContext::TextThread {
text_thread,
name: _,
} => {
if let Some(text_thread) = text_thread.upgrade() {
let context_id = self.next_context_id.post_inc();
self.insert_context(
AgentContextHandle::TextThread(TextThreadContextHandle {
text_thread,
context_id,
}),
cx,
);
}
}
}
}
fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
match &context {
// AgentContextHandle::Thread(thread_context) => {
// if let Some(thread_store) = self.thread_store.clone() {
// thread_context.thread.update(cx, |thread, cx| {
// thread.start_generating_detailed_summary_if_needed(thread_store, cx);
// });
// self.context_thread_ids
// .insert(thread_context.thread.read(cx).id().clone());
// } else {
// return false;
// }
// }
AgentContextHandle::TextThread(text_thread_context) => {
self.context_text_thread_paths
.extend(text_thread_context.text_thread.read(cx).path().cloned());
}
_ => {}
}
let inserted = self.context_set.insert(AgentContextKey(context));
if inserted {
cx.notify();
}
inserted
}
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
if let Some((_, key)) = self
.context_set
.shift_remove_full(AgentContextKey::ref_cast(context))
{
match context {
AgentContextHandle::Thread(thread_context) => {
self.context_thread_ids
.remove(thread_context.thread.read(cx).id());
}
AgentContextHandle::TextThread(text_thread_context) => {
if let Some(path) = text_thread_context.text_thread.read(cx).path() {
self.context_text_thread_paths.remove(path);
}
}
_ => {}
}
cx.emit(ContextStoreEvent::ContextRemoved(key));
cx.notify();
}
}
pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
self.context_set
.contains(AgentContextKey::ref_cast(context))
}
/// Returns whether this file path is already included directly in the context, or if it will be
/// included in the context via a directory.
pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
let project = self.project.upgrade()?.read(cx);
self.context().find_map(|context| match context {
AgentContextHandle::File(file_context) => {
FileInclusion::check_file(file_context, path, cx)
}
AgentContextHandle::Image(image_context) => {
FileInclusion::check_image(image_context, path)
}
AgentContextHandle::Directory(directory_context) => {
FileInclusion::check_directory(directory_context, path, project, cx)
}
_ => None,
})
}
pub fn path_included_in_directory(
&self,
path: &ProjectPath,
cx: &App,
) -> Option<FileInclusion> {
let project = self.project.upgrade()?.read(cx);
self.context().find_map(|context| match context {
AgentContextHandle::Directory(directory_context) => {
FileInclusion::check_directory(directory_context, path, project, cx)
}
_ => None,
})
}
pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
self.context().any(|context| match context {
AgentContextHandle::Symbol(context) => {
if context.symbol != symbol.name {
return false;
}
let buffer = context.buffer.read(cx);
let Some(context_path) = buffer.project_path(cx) else {
return false;
};
if symbol.path != SymbolLocation::InProject(context_path) {
return false;
}
let context_range = context.range.to_point_utf16(&buffer.snapshot());
context_range.start == symbol.range.start.0
&& context_range.end == symbol.range.end.0
}
_ => false,
})
}
pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
self.context_thread_ids.contains(thread_id)
}
pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
self.context_text_thread_paths.contains(path)
}
pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
self.context_set
.contains(&RulesContextHandle::lookup_key(prompt_id))
}
pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
self.context_set
.contains(&FetchedUrlContext::lookup_key(url.into()))
}
pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
self.context_set
.get(&FetchedUrlContext::lookup_key(url))
.map(|key| key.as_ref().clone())
}
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
self.context()
.filter_map(|context| match context {
AgentContextHandle::File(file) => {
let buffer = file.buffer.read(cx);
buffer.project_path(cx)
}
AgentContextHandle::Directory(_)
| AgentContextHandle::Symbol(_)
| AgentContextHandle::Thread(_)
| AgentContextHandle::Selection(_)
| AgentContextHandle::FetchedUrl(_)
| AgentContextHandle::TextThread(_)
| AgentContextHandle::Rules(_)
| AgentContextHandle::Image(_) => None,
})
.collect()
}
pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
&self.context_thread_ids
}
}
#[derive(Clone)]
pub enum SuggestedContext {
File {
name: SharedString,
icon_path: Option<SharedString>,
buffer: WeakEntity<Buffer>,
},
TextThread {
name: SharedString,
text_thread: WeakEntity<TextThread>,
},
}
impl SuggestedContext {
pub fn name(&self) -> &SharedString {
match self {
Self::File { name, .. } => name,
Self::TextThread { name, .. } => name,
}
}
pub fn icon_path(&self) -> Option<SharedString> {
match self {
Self::File { icon_path, .. } => icon_path.clone(),
Self::TextThread { .. } => None,
}
}
pub fn kind(&self) -> ContextKind {
match self {
Self::File { .. } => ContextKind::File,
Self::TextThread { .. } => ContextKind::TextThread,
}
}
}
pub enum FileInclusion {
Direct,
InDirectory { full_path: PathBuf },
}
impl FileInclusion {
fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
let file_path = file_context.buffer.read(cx).project_path(cx)?;
if path == &file_path {
Some(FileInclusion::Direct)
} else {
None
}
}
fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
let image_path = image_context.project_path.as_ref()?;
if path == image_path {
Some(FileInclusion::Direct)
} else {
None
}
}
fn check_directory(
directory_context: &DirectoryContextHandle,
path: &ProjectPath,
project: &Project,
cx: &App,
) -> Option<Self> {
let worktree = project
.worktree_for_entry(directory_context.entry_id, cx)?
.read(cx);
let entry = worktree.entry_for_id(directory_context.entry_id)?;
let directory_path = ProjectPath {
worktree_id: worktree.id(),
path: entry.path.clone(),
};
if path.starts_with(&directory_path) {
if path == &directory_path {
Some(FileInclusion::Direct)
} else {
Some(FileInclusion::InDirectory {
full_path: worktree.full_path(&entry.path),
})
}
} else {
None
}
}
}

View File

@@ -1,619 +0,0 @@
use crate::{
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
context_picker::ContextPicker,
ui::{AddedContext, ContextPill},
};
use crate::{
context::AgentContextHandle,
context_store::{ContextStore, SuggestedContext},
};
use agent::HistoryStore;
use collections::HashSet;
use editor::Editor;
use gpui::{
App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, Task, WeakEntity,
};
use itertools::Itertools;
use project::ProjectItem;
use prompt_store::PromptStore;
use rope::Point;
use std::rc::Rc;
use text::ToPoint as _;
use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
use util::ResultExt as _;
use workspace::Workspace;
use zed_actions::assistant::OpenRulesLibrary;
pub struct ContextStrip {
context_store: Entity<ContextStore>,
context_picker: Entity<ContextPicker>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
focus_handle: FocusHandle,
suggest_context_kind: SuggestContextKind,
workspace: WeakEntity<Workspace>,
prompt_store: Option<WeakEntity<PromptStore>>,
_subscriptions: Vec<Subscription>,
focused_index: Option<usize>,
children_bounds: Option<Vec<Bounds<Pixels>>>,
model_usage_context: ModelUsageContext,
}
impl ContextStrip {
pub fn new(
context_store: Entity<ContextStore>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
suggest_context_kind: SuggestContextKind,
model_usage_context: ModelUsageContext,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let context_picker = cx.new(|cx| {
ContextPicker::new(
workspace.clone(),
thread_store.clone(),
prompt_store.clone(),
context_store.downgrade(),
window,
cx,
)
});
let focus_handle = cx.focus_handle();
let subscriptions = vec![
cx.observe(&context_store, |_, _, cx| cx.notify()),
cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
cx.on_focus(&focus_handle, window, Self::handle_focus),
cx.on_blur(&focus_handle, window, Self::handle_blur),
];
Self {
context_store: context_store.clone(),
context_picker,
context_picker_menu_handle,
focus_handle,
suggest_context_kind,
workspace,
prompt_store,
_subscriptions: subscriptions,
focused_index: None,
children_bounds: None,
model_usage_context,
}
}
/// Whether or not the context strip has items to display
pub fn has_context_items(&self, cx: &App) -> bool {
self.context_store.read(cx).context().next().is_some()
|| self.suggested_context(cx).is_some()
}
fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
if let Some(workspace) = self.workspace.upgrade() {
let project = workspace.read(cx).project().read(cx);
let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
let current_model = self.model_usage_context.language_model(cx);
self.context_store
.read(cx)
.context()
.flat_map(|context| {
AddedContext::new_pending(
context.clone(),
prompt_store.as_ref(),
project,
current_model.as_ref(),
cx,
)
})
.collect::<Vec<_>>()
} else {
Vec::new()
}
}
fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
match self.suggest_context_kind {
SuggestContextKind::Thread => self.suggested_thread(cx),
}
}
fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
if !self.context_picker.read(cx).allow_threads() {
return None;
}
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
let text_thread = active_text_thread_editor.read(cx).text_thread();
let weak_text_thread = text_thread.downgrade();
let text_thread = text_thread.read(cx);
let path = text_thread.path()?;
if self.context_store.read(cx).includes_text_thread(path) {
return None;
}
Some(SuggestedContext::TextThread {
name: text_thread.summary().or_default(),
text_thread: weak_text_thread,
})
} else {
None
}
}
fn handle_context_picker_event(
&mut self,
_picker: &Entity<ContextPicker>,
_event: &DismissEvent,
_window: &mut Window,
cx: &mut Context<Self>,
) {
cx.emit(ContextStripEvent::PickerDismissed);
}
fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = self.last_pill_index();
cx.notify();
}
fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = None;
cx.notify();
}
fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
self.focused_index = match self.focused_index {
Some(index) if index > 0 => Some(index - 1),
_ => self.last_pill_index(),
};
cx.notify();
}
fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
let Some(last_index) = self.last_pill_index() else {
return;
};
self.focused_index = match self.focused_index {
Some(index) if index < last_index => Some(index + 1),
_ => Some(0),
};
cx.notify();
}
fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
let Some(focused_index) = self.focused_index else {
return;
};
if focused_index == 0 {
return cx.emit(ContextStripEvent::BlurredUp);
}
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
return;
};
let iter = pills[..focused_index].iter().enumerate().rev();
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
cx.notify();
}
fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
let Some(focused_index) = self.focused_index else {
return;
};
let last_index = self.last_pill_index();
if self.focused_index == last_index {
return cx.emit(ContextStripEvent::BlurredDown);
}
let Some((focused, pills)) = self.focused_bounds(focused_index) else {
return;
};
let iter = pills.iter().enumerate().skip(focused_index + 1);
self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
cx.notify();
}
fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
let pill_bounds = self.pill_bounds()?;
let focused = pill_bounds.get(focused)?;
Some((focused, pill_bounds))
}
fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
let bounds = self.children_bounds.as_ref()?;
let eraser = if bounds.len() < 3 { 0 } else { 1 };
let pills = &bounds[1..bounds.len() - eraser];
if pills.is_empty() { None } else { Some(pills) }
}
fn last_pill_index(&self) -> Option<usize> {
Some(self.pill_bounds()?.len() - 1)
}
fn find_best_horizontal_match<'a>(
focused: &'a Bounds<Pixels>,
iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
) -> Option<usize> {
let mut best = None;
let focused_left = focused.left();
let focused_right = focused.right();
for (index, probe) in iter {
if probe.origin.y == focused.origin.y {
continue;
}
let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
best = match best {
Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
break;
}
Some(_) | None => Some((index, overlap, probe.origin.y)),
};
}
best.map(|(index, _, _)| index)
}
fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
match context {
AgentContextHandle::File(file_context) => {
if let Some(project_path) = file_context.project_path(cx) {
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
});
}
}
AgentContextHandle::Directory(directory_context) => {
let entry_id = directory_context.entry_id;
workspace.update(cx, |workspace, cx| {
workspace.project().update(cx, |_project, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id));
})
})
}
AgentContextHandle::Symbol(symbol_context) => {
let buffer = symbol_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot();
let target_position = symbol_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AgentContextHandle::Selection(selection_context) => {
let buffer = selection_context.buffer.read(cx);
if let Some(project_path) = buffer.project_path(cx) {
let snapshot = buffer.snapshot();
let target_position = selection_context.range.start.to_point(&snapshot);
open_editor_at_position(project_path, target_position, &workspace, window, cx)
.detach();
}
}
AgentContextHandle::FetchedUrl(fetched_url_context) => {
cx.open_url(&fetched_url_context.url);
}
AgentContextHandle::Thread(_thread_context) => {}
AgentContextHandle::TextThread(text_thread_context) => {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
let context = text_thread_context.text_thread.clone();
window.defer(cx, move |window, cx| {
panel.update(cx, |panel, cx| {
panel.open_text_thread(context, window, cx)
});
});
}
})
}
AgentContextHandle::Rules(rules_context) => window.dispatch_action(
Box::new(OpenRulesLibrary {
prompt_to_select: Some(rules_context.prompt_id.0),
}),
cx,
),
AgentContextHandle::Image(_) => {}
}
}
fn remove_focused_context(
&mut self,
_: &RemoveFocusedContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(index) = self.focused_index {
let added_contexts = self.added_contexts(cx);
let Some(context) = added_contexts.get(index) else {
return;
};
self.context_store.update(cx, |this, cx| {
this.remove_context(&context.handle, cx);
});
let is_now_empty = added_contexts.len() == 1;
if is_now_empty {
cx.emit(ContextStripEvent::BlurredEmpty);
} else {
self.focused_index = Some(index.saturating_sub(1));
cx.notify();
}
}
}
fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
// We only suggest one item after the actual context
self.focused_index == Some(added_contexts.len())
}
fn accept_suggested_context(
&mut self,
_: &AcceptSuggestedContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(suggested) = self.suggested_context(cx)
&& self.is_suggested_focused(&self.added_contexts(cx))
{
self.add_suggested_context(&suggested, cx);
}
}
fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
self.context_store.update(cx, |context_store, cx| {
context_store.add_suggested_context(suggested, cx)
});
cx.notify();
}
}
impl Focusable for ContextStrip {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ContextStrip {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let context_picker = self.context_picker.clone();
let focus_handle = self.focus_handle.clone();
let added_contexts = self.added_contexts(cx);
let dupe_names = added_contexts
.iter()
.map(|c| c.name.clone())
.sorted()
.tuple_windows()
.filter(|(a, b)| a == b)
.map(|(a, _)| a)
.collect::<HashSet<SharedString>>();
let no_added_context = added_contexts.is_empty();
let suggested_context = self.suggested_context(cx).map(|suggested_context| {
(
suggested_context,
self.is_suggested_focused(&added_contexts),
)
});
h_flex()
.flex_wrap()
.gap_1()
.track_focus(&focus_handle)
.key_context("ContextStrip")
.on_action(cx.listener(Self::focus_up))
.on_action(cx.listener(Self::focus_right))
.on_action(cx.listener(Self::focus_down))
.on_action(cx.listener(Self::focus_left))
.on_action(cx.listener(Self::remove_focused_context))
.on_action(cx.listener(Self::accept_suggested_context))
.on_children_prepainted({
let entity = cx.entity().downgrade();
move |children_bounds, _window, cx| {
entity
.update(cx, |this, _| {
this.children_bounds = Some(children_bounds);
})
.ok();
}
})
.child(
PopoverMenu::new("context-picker")
.menu({
let context_picker = context_picker.clone();
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(context_picker.clone())
}
})
.on_open({
let context_picker = context_picker.downgrade();
Rc::new(move |window, cx| {
context_picker
.update(cx, |context_picker, cx| {
context_picker.select_first(window, cx);
})
.ok();
})
})
.trigger_with_tooltip(
IconButton::new("add-context", IconName::Plus)
.icon_size(IconSize::Small)
.style(ui::ButtonStyle::Filled),
{
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Add Context",
&ToggleContextPicker,
&focus_handle,
cx,
)
}
},
)
.attach(gpui::Corner::TopLeft)
.anchor(gpui::Corner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-2.0),
})
.with_handle(self.context_picker_menu_handle.clone()),
)
.children(
added_contexts
.into_iter()
.enumerate()
.map(|(i, added_context)| {
let name = added_context.name.clone();
let context = added_context.handle.clone();
ContextPill::added(
added_context,
dupe_names.contains(&name),
self.focused_index == Some(i),
Some({
let context = context.clone();
let context_store = self.context_store.clone();
Rc::new(cx.listener(move |_this, _event, _window, cx| {
context_store.update(cx, |this, cx| {
this.remove_context(&context, cx);
});
cx.notify();
}))
}),
)
.on_click({
Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
if event.click_count() > 1 {
this.open_context(&context, window, cx);
} else {
this.focused_index = Some(i);
}
cx.notify();
}))
})
}),
)
.when_some(suggested_context, |el, (suggested, focused)| {
el.child(
ContextPill::suggested(
suggested.name().clone(),
suggested.icon_path(),
suggested.kind(),
focused,
)
.on_click(Rc::new(cx.listener(
move |this, _event, _window, cx| {
this.add_suggested_context(&suggested, cx);
},
))),
)
})
.when(!no_added_context, {
move |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Remove All Context",
&RemoveAllContext,
&focus_handle,
cx,
)
}
})
.on_click(cx.listener({
let focus_handle = focus_handle.clone();
move |_this, _event, window, cx| {
focus_handle.dispatch_action(&RemoveAllContext, window, cx);
}
})),
)
}
})
.into_any()
}
}
pub enum ContextStripEvent {
PickerDismissed,
BlurredEmpty,
BlurredDown,
BlurredUp,
}
impl EventEmitter<ContextStripEvent> for ContextStrip {}
pub enum SuggestContextKind {
Thread,
}
fn open_editor_at_position(
project_path: project::ProjectPath,
target_position: Point,
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<()> {
let open_task = workspace.update(cx, |workspace, cx| {
workspace.open_path(project_path, None, true, window, cx)
});
window.spawn(cx, async move |cx| {
if let Some(active_editor) = open_task
.await
.log_err()
.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update_in(cx, |editor, window, cx| {
editor.go_to_singleton_buffer_point(target_position, window, cx);
})
.log_err();
}
})
}

View File

@@ -4,10 +4,11 @@ use std::ops::Range;
use std::rc::Rc;
use std::sync::Arc;
use crate::context::load_context;
use crate::mention_set::MentionSet;
use crate::{
AgentPanel,
buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
context_store::ContextStore,
inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
terminal_inline_assistant::TerminalInlineAssistant,
};
@@ -16,6 +17,8 @@ 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::MultiBufferOffset;
use editor::RowExt;
use editor::SelectionEffects;
use editor::scroll::ScrollOffset;
@@ -29,6 +32,7 @@ use editor::{
},
};
use fs::Fs;
use futures::FutureExt;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
@@ -212,16 +216,10 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
if is_ai_enabled {
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
let thread_store = panel
.as_ref()
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider {
editor: cx.entity().downgrade(),
workspace: workspace.downgrade(),
thread_store,
}),
window,
cx,
@@ -233,9 +231,6 @@ impl InlineAssistant {
editor.cancel(&Default::default(), window, cx);
}
}
// Remove the Assistant1 code action provider, as it still might be registered.
editor.remove_code_action_provider("assistant".into(), window, cx);
} else {
editor.remove_code_action_provider(
ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
@@ -277,8 +272,7 @@ impl InlineAssistant {
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 thread_store = agent_panel.thread_store().clone();
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -287,10 +281,9 @@ impl InlineAssistant {
assistant.assist(
&active_editor,
cx.entity().downgrade(),
context_store,
workspace.project().downgrade(),
prompt_store,
thread_store,
prompt_store,
action.prompt.clone(),
window,
cx,
@@ -303,8 +296,8 @@ impl InlineAssistant {
&active_terminal,
cx.entity().downgrade(),
workspace.project().downgrade(),
prompt_store,
thread_store,
prompt_store,
action.prompt.clone(),
window,
cx,
@@ -350,25 +343,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 +369,7 @@ impl InlineAssistant {
&& newest_selection.end.row <= range.end.row
{
self.focus_assist(*assist_id, window, cx);
return;
return None;
}
}
}
@@ -452,7 +440,6 @@ impl InlineAssistant {
{
let anchor_range = Anchor::range_in_buffer(
excerpt_id,
buffer.remote_id(),
buffer.anchor_before(buffer_range.start)..buffer.anchor_after(buffer_range.end),
);
@@ -473,6 +460,25 @@ impl InlineAssistant {
}
}
Some((codegen_ranges, newest_selection))
}
fn batch_assist(
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
initial_prompt: Option<String>,
window: &mut Window,
codegen_ranges: &[Range<Anchor>],
newest_selection: Option<Selection<Point>>,
initial_transaction_id: Option<TransactionId>,
cx: &mut App,
) -> Option<InlineAssistId> {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let assist_group_id = self.next_assist_group_id.post_inc();
let prompt_buffer = cx.new(|cx| {
MultiBuffer::singleton(
@@ -483,16 +489,14 @@ 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(),
initial_transaction_id,
self.telemetry.clone(),
self.prompt_builder.clone(),
cx,
@@ -508,20 +512,22 @@ impl InlineAssistant {
prompt_buffer.clone(),
codegen.clone(),
self.fs.clone(),
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store.as_ref().map(|s| s.downgrade()),
prompt_store.clone(),
project.clone(),
workspace.clone(),
window,
cx,
)
});
if assist_to_focus.is_none() {
if let Some(newest_selection) = newest_selection.as_ref()
&& 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);
@@ -533,7 +539,7 @@ impl InlineAssistant {
assists.push((
assist_id,
range,
range.clone(),
prompt_editor,
prompt_block_id,
end_block_id,
@@ -544,6 +550,15 @@ impl InlineAssistant {
.assists_by_editor
.entry(editor.downgrade())
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
let assist_to_focus = if let Some(focus_id) = assist_to_focus {
Some(focus_id)
} else if assists.len() >= 1 {
Some(assists[0].0)
} else {
None
};
let mut assist_group = InlineAssistGroup::new();
for (assist_id, range, prompt_editor, prompt_block_id, end_block_id) in assists {
let codegen = prompt_editor.read(cx).codegen().clone();
@@ -567,8 +582,45 @@ impl InlineAssistant {
assist_group.assist_ids.push(assist_id);
editor_assists.assist_ids.push(assist_id);
}
self.assist_groups.insert(assist_group_id, assist_group);
assist_to_focus
}
pub fn assist(
&mut self,
editor: &Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
initial_prompt: Option<String>,
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_to_focus = self.batch_assist(
editor,
workspace,
project,
thread_store,
prompt_store,
initial_prompt,
window,
&codegen_ranges,
Some(newest_selection),
None,
cx,
);
if let Some(assist_id) = assist_to_focus {
self.focus_assist(assist_id, window, cx);
}
@@ -582,17 +634,11 @@ impl InlineAssistant {
initial_transaction_id: Option<TransactionId>,
focus: bool,
workspace: Entity<Workspace>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<HistoryStore>>,
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();
let buffer = editor.read(cx).buffer().clone();
{
let snapshot = buffer.read(cx).read(cx);
@@ -601,68 +647,22 @@ impl InlineAssistant {
}
let project = workspace.read(cx).project().downgrade();
let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
let codegen = cx.new(|cx| {
BufferCodegen::new(
editor.read(cx).buffer().clone(),
range.clone(),
initial_transaction_id,
context_store.clone(),
project,
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,
workspace.downgrade(),
thread_store,
prompt_store.map(|s| s.downgrade()),
window,
cx,
)
});
let [prompt_block_id, end_block_id] =
self.insert_assist_blocks(editor, &range, &prompt_editor, cx);
let editor_assists = self
.assists_by_editor
.entry(editor.downgrade())
.or_insert_with(|| EditorInlineAssists::new(editor, window, cx));
let mut assist_group = InlineAssistGroup::new();
self.assists.insert(
assist_id,
InlineAssist::new(
assist_id,
assist_group_id,
let assist_id = self
.batch_assist(
editor,
&prompt_editor,
prompt_block_id,
end_block_id,
range,
codegen.clone(),
workspace.downgrade(),
project,
thread_store,
prompt_store,
Some(initial_prompt),
window,
&[range],
None,
initial_transaction_id,
cx,
),
);
assist_group.assist_ids.push(assist_id);
editor_assists.assist_ids.push(assist_id);
self.assist_groups.insert(assist_group_id, assist_group);
)
.expect("batch_assist returns an id if there's only one range");
if focus {
self.focus_assist(assist_id, window, cx);
@@ -803,7 +803,7 @@ impl InlineAssistant {
(
editor
.selections
.newest::<usize>(&editor.display_snapshot(cx)),
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)),
editor.buffer().read(cx).snapshot(cx),
)
});
@@ -836,7 +836,7 @@ impl InlineAssistant {
(
editor
.selections
.newest::<usize>(&editor.display_snapshot(cx)),
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx)),
editor.buffer().read(cx).snapshot(cx),
)
});
@@ -853,12 +853,14 @@ impl InlineAssistant {
} else {
let distance_from_selection = assist_range
.start
.abs_diff(selection.start)
.min(assist_range.start.abs_diff(selection.end))
.0
.abs_diff(selection.start.0)
.min(assist_range.start.0.abs_diff(selection.end.0))
+ assist_range
.end
.abs_diff(selection.start)
.min(assist_range.end.abs_diff(selection.end));
.0
.abs_diff(selection.start.0)
.min(assist_range.end.0.abs_diff(selection.end.0));
match closest_assist_fallback {
Some((_, old_distance)) => {
if distance_from_selection < old_distance {
@@ -935,7 +937,7 @@ impl InlineAssistant {
EditorEvent::Edited { transaction_id } => {
let buffer = editor.read(cx).buffer().read(cx);
let edited_ranges =
buffer.edited_ranges_for_transaction::<usize>(*transaction_id, cx);
buffer.edited_ranges_for_transaction::<MultiBufferOffset>(*transaction_id, cx);
let snapshot = buffer.snapshot(cx);
for assist_id in editor_assists.assist_ids.clone() {
@@ -1274,7 +1276,8 @@ impl InlineAssistant {
return;
}
let Some(user_prompt) = assist.user_prompt(cx) else {
let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx))
else {
return;
};
@@ -1290,9 +1293,12 @@ impl InlineAssistant {
return;
};
let context_task = load_context(&mention_set, cx).shared();
assist
.codegen
.update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
.update(cx, |codegen, cx| {
codegen.start(model, user_prompt, context_task, cx)
})
.log_err();
}
@@ -1438,6 +1444,7 @@ impl InlineAssistant {
multi_buffer.update(cx, |multi_buffer, cx| {
multi_buffer.push_excerpts(
old_buffer.clone(),
// todo(lw): buffer_start and buffer_end might come from different snapshots!
Some(ExcerptRange::new(buffer_start..buffer_end)),
cx,
);
@@ -1758,6 +1765,11 @@ impl InlineAssist {
let decorations = self.decorations.as_ref()?;
Some(decorations.prompt_editor.read(cx).prompt(cx))
}
fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
let decorations = self.decorations.as_ref()?;
Some(decorations.prompt_editor.read(cx).mention_set().clone())
}
}
struct InlineAssistDecorations {
@@ -1770,10 +1782,9 @@ struct InlineAssistDecorations {
struct AssistantCodeActionProvider {
editor: WeakEntity<Editor>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
}
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
impl CodeActionProvider for AssistantCodeActionProvider {
fn id(&self) -> Arc<str> {
@@ -1841,10 +1852,20 @@ impl CodeActionProvider for AssistantCodeActionProvider {
) -> Task<Result<ProjectTransaction>> {
let editor = self.editor.clone();
let workspace = self.workspace.clone();
let thread_store = self.thread_store.clone();
let prompt_store = PromptStore::global(cx);
window.spawn(cx, async move |cx| {
let workspace = workspace.upgrade().context("workspace was released")?;
let thread_store = cx.update(|_window, cx| {
anyhow::Ok(
workspace
.read(cx)
.panel::<AgentPanel>(cx)
.context("missing agent panel")?
.read(cx)
.thread_store()
.clone(),
)
})??;
let editor = editor.upgrade().context("editor was released")?;
let range = editor
.update(cx, |editor, cx| {
@@ -1887,8 +1908,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
None,
true,
workspace,
prompt_store,
thread_store,
prompt_store,
window,
cx,
);

View File

@@ -1,19 +1,21 @@
use agent::HistoryStore;
use collections::{HashMap, VecDeque};
use editor::actions::Paste;
use editor::code_context_menus::CodeContextMenu;
use editor::display_map::{CreaseId, EditorMargins};
use editor::{Addon, AnchorRangeExt as _};
use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp},
};
use fs::Fs;
use gpui::{
AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
Focusable, Subscription, TextStyle, WeakEntity, Window,
AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, TextStyle, WeakEntity, Window,
};
use language_model::{LanguageModel, LanguageModelRegistry};
use parking_lot::Mutex;
use project::Project;
use prompt_store::PromptStore;
use settings::Settings;
use std::cmp;
@@ -28,22 +30,21 @@ use zed_actions::agent::ToggleModelSelector;
use crate::agent_model_selector::AgentModelSelector;
use crate::buffer_codegen::BufferCodegen;
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::terminal_codegen::TerminalCodegen;
use crate::{
CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
ToggleContextPicker,
use crate::completion_provider::{
PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
};
use crate::mention_set::paste_images_as_context;
use crate::mention_set::{MentionSet, crease_for_mention};
use crate::terminal_codegen::TerminalCodegen;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
pub struct PromptEditor<T> {
pub editor: Entity<Editor>,
mode: PromptEditorMode,
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
mention_set: Entity<MentionSet>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
model_selector: Entity<AgentModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
@@ -51,7 +52,6 @@ pub struct PromptEditor<T> {
pending_prompt: String,
_codegen_subscription: Subscription,
editor_subscriptions: Vec<Subscription>,
_context_strip_subscription: Subscription,
show_rate_limit_notice: bool,
_phantom: std::marker::PhantomData<T>,
}
@@ -98,6 +98,19 @@ impl<T: 'static> Render for PromptEditor<T> {
buttons.extend(self.render_buttons(window, cx));
let menu_visible = self.is_completions_menu_visible(cx);
let add_context_button = 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| {
this.trigger_completion_menu(window, cx);
}));
v_flex()
.key_context("PromptEditor")
.capture_action(cx.listener(Self::paste))
@@ -114,7 +127,6 @@ impl<T: 'static> Render for PromptEditor<T> {
h_flex()
.items_start()
.cursor(CursorStyle::Arrow)
.on_action(cx.listener(Self::toggle_context_picker))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
@@ -123,7 +135,6 @@ impl<T: 'static> Render for PromptEditor<T> {
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.on_action(cx.listener(Self::remove_all_context))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.child(
@@ -182,7 +193,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.pl_1()
.items_start()
.justify_between()
.child(self.context_strip.clone())
.child(add_context_button)
.child(self.model_selector.clone()),
),
)
@@ -214,6 +225,19 @@ impl<T: 'static> PromptEditor<T> {
));
}
fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
PromptEditorCompletionProviderDelegate,
cx.weak_entity(),
self.mention_set.clone(),
self.history_store.clone(),
self.prompt_store.clone(),
self.workspace.clone(),
))));
});
}
pub fn set_show_cursor_when_unfocused(
&mut self,
show_cursor_when_unfocused: bool,
@@ -226,27 +250,40 @@ impl<T: 'static> PromptEditor<T> {
pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let prompt = self.prompt(cx);
let existing_creases = self.editor.update(cx, extract_message_creases);
let existing_creases = self.editor.update(cx, |editor, cx| {
extract_message_creases(editor, &self.mention_set, window, cx)
});
let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
let mut creases = vec![];
self.editor = cx.new(|cx| {
let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text("Add a prompt…", window, cx);
editor.set_text(prompt, window, cx);
insert_message_creases(
&mut editor,
&existing_creases,
&self.context_store,
window,
cx,
);
creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
if focus {
window.focus(&editor.focus_handle(cx));
}
editor
});
self.mention_set.update(cx, |mention_set, _cx| {
debug_assert_eq!(
creases.len(),
mention_set.creases().len(),
"Missing creases"
);
let mentions = mention_set
.clear()
.zip(creases)
.map(|((_, value), id)| (id, value))
.collect::<HashMap<_, _>>();
mention_set.set_mentions(mentions);
});
self.assign_completion_provider(cx);
self.subscribe_to_editor(window, cx);
}
@@ -274,43 +311,29 @@ impl<T: 'static> PromptEditor<T> {
self.editor.read(cx).text(cx)
}
fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
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() {
return;
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
if inline_assistant_model_supports_images(cx)
&& let Some(task) =
paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
{
task.detach();
}
cx.stop_propagation();
self.context_store.update(cx, |store, cx| {
for image in images {
store.add_image_instance(Arc::new(image), cx);
}
});
}
fn handle_prompt_editor_events(
&mut self,
_: &Entity<Editor>,
editor: &Entity<Editor>,
event: &EditorEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
EditorEvent::Edited { .. } => {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
self.mention_set
.update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
@@ -321,7 +344,7 @@ impl<T: 'static> PromptEditor<T> {
.log_edit_event("inline assist", is_via_ssh);
});
}
let prompt = self.editor.read(cx).text(cx);
let prompt = snapshot.text();
if self
.prompt_history_ix
.is_none_or(|ix| self.prompt_history[ix] != prompt)
@@ -343,23 +366,44 @@ impl<T: 'static> PromptEditor<T> {
}
}
fn toggle_context_picker(
&mut self,
_: &ToggleContextPicker,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_picker_menu_handle.toggle(window, 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 remove_all_context(
&mut self,
_: &RemoveAllContext,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.context_store.update(cx, |store, cx| store.clear(cx));
cx.notify();
pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, 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 > 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);
});
}
fn cancel(
@@ -434,8 +478,6 @@ impl<T: 'static> PromptEditor<T> {
editor.move_to_end(&Default::default(), window, cx)
});
}
} else if self.context_strip.read(cx).has_context_items(cx) {
self.context_strip.focus_handle(cx).focus(window);
}
}
@@ -709,6 +751,7 @@ impl<T: 'static> PromptEditor<T> {
EditorStyle {
background: colors.editor_background,
local_player: cx.theme().players().local(),
syntax: cx.theme().syntax().clone(),
text: text_style,
..Default::default()
},
@@ -716,21 +759,6 @@ impl<T: 'static> PromptEditor<T> {
})
.into_any_element()
}
fn handle_context_strip_event(
&mut self,
_context_strip: &Entity<ContextStrip>,
event: &ContextStripEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
ContextStripEvent::PickerDismissed
| ContextStripEvent::BlurredEmpty
| ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
ContextStripEvent::BlurredDown => {}
}
}
}
pub enum PromptEditorMode {
@@ -765,6 +793,36 @@ impl InlineAssistId {
}
}
struct PromptEditorCompletionProviderDelegate;
fn inline_assistant_model_supports_images(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.map_or(false, |m| m.model.supports_images())
}
impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
vec![
PromptContextType::File,
PromptContextType::Symbol,
PromptContextType::Thread,
PromptContextType::Fetch,
PromptContextType::Rules,
]
}
fn supports_images(&self, cx: &App) -> bool {
inline_assistant_model_supports_images(cx)
}
fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
Vec::new()
}
fn confirm_command(&self, _cx: &mut App) {}
}
impl PromptEditor<BufferCodegen> {
pub fn new_buffer(
id: InlineAssistId,
@@ -773,15 +831,14 @@ impl PromptEditor<BufferCodegen> {
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
fs: Arc<dyn Fs>,
context_store: Entity<ContextStore>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window,
cx: &mut Context<PromptEditor<BufferCodegen>>,
) -> PromptEditor<BufferCodegen> {
let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
let mode = PromptEditorMode::Buffer {
id,
codegen,
@@ -805,7 +862,6 @@ impl PromptEditor<BufferCodegen> {
// typing in one will make what you typed appear in all of them.
editor.set_show_cursor_when_unfocused(true, cx);
editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
editor.register_addon(ContextCreasesAddon::new());
editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12,
max_entries_visible: 12,
@@ -815,43 +871,17 @@ impl PromptEditor<BufferCodegen> {
editor
});
let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
prompt_store.clone(),
prompt_editor_entity,
codegen_buffer.as_ref().map(Entity::downgrade),
))));
});
let mention_set =
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store,
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
ModelUsageContext::InlineAssistant,
window,
cx,
)
});
let context_strip_subscription =
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
let mut this: PromptEditor<BufferCodegen> = PromptEditor {
editor: prompt_editor.clone(),
context_store,
context_strip,
context_picker_menu_handle,
mention_set,
history_store,
prompt_store,
workspace,
model_selector: cx.new(|cx| {
AgentModelSelector::new(
fs,
@@ -868,12 +898,12 @@ impl PromptEditor<BufferCodegen> {
pending_prompt: String::new(),
_codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(),
_context_strip_subscription: context_strip_subscription,
show_rate_limit_notice: false,
mode,
_phantom: Default::default(),
};
this.assign_completion_provider(cx);
this.subscribe_to_editor(window, cx);
this
}
@@ -919,6 +949,10 @@ impl PromptEditor<BufferCodegen> {
}
}
pub fn mention_set(&self) -> &Entity<MentionSet> {
&self.mention_set
}
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
match &self.mode {
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
@@ -945,10 +979,10 @@ impl PromptEditor<TerminalCodegen> {
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<TerminalCodegen>,
fs: Arc<dyn Fs>,
context_store: Entity<ContextStore>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<HistoryStore>>,
prompt_store: Option<WeakEntity<PromptStore>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -980,43 +1014,17 @@ impl PromptEditor<TerminalCodegen> {
editor
});
let prompt_editor_entity = prompt_editor.downgrade();
prompt_editor.update(cx, |editor, _| {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
workspace.clone(),
context_store.downgrade(),
thread_store.clone(),
prompt_store.clone(),
prompt_editor_entity,
None,
))));
});
let mention_set =
cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone()));
let context_picker_menu_handle = PopoverMenuHandle::default();
let model_selector_menu_handle = PopoverMenuHandle::default();
let context_strip = cx.new(|cx| {
ContextStrip::new(
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store.clone(),
context_picker_menu_handle.clone(),
SuggestContextKind::Thread,
ModelUsageContext::InlineAssistant,
window,
cx,
)
});
let context_strip_subscription =
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
let mut this = Self {
editor: prompt_editor.clone(),
context_store,
context_strip,
context_picker_menu_handle,
mention_set,
history_store,
prompt_store,
workspace,
model_selector: cx.new(|cx| {
AgentModelSelector::new(
fs,
@@ -1033,12 +1041,12 @@ impl PromptEditor<TerminalCodegen> {
pending_prompt: String::new(),
_codegen_subscription: codegen_subscription,
editor_subscriptions: Vec::new(),
_context_strip_subscription: context_strip_subscription,
mode,
show_rate_limit_notice: false,
_phantom: Default::default(),
};
this.count_lines(cx);
this.assign_completion_provider(cx);
this.subscribe_to_editor(window, cx);
this
}
@@ -1085,6 +1093,10 @@ impl PromptEditor<TerminalCodegen> {
}
}
pub fn mention_set(&self) -> &Entity<MentionSet> {
&self.mention_set
}
pub fn codegen(&self) -> &Entity<TerminalCodegen> {
match &self.mode {
PromptEditorMode::Buffer { .. } => unreachable!(),
@@ -1164,131 +1176,41 @@ 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>,
struct MessageCrease {
range: Range<MultiBufferOffset>,
icon_path: SharedString,
label: SharedString,
}
#[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(
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>,
mention_set: &Entity<MentionSet>,
window: &mut Window,
cx: &mut Context<'_, Editor>,
) {
) -> Vec<MessageCrease> {
let creases = mention_set.read(cx).creases();
let snapshot = editor.snapshot(window, cx);
snapshot
.crease_snapshot
.creases()
.filter(|(id, _)| creases.contains(id))
.filter_map(|(_, crease)| {
let metadata = crease.metadata()?.clone();
Some(MessageCrease {
range: crease.range().to_offset(snapshot.buffer()),
label: metadata.label,
icon_path: metadata.icon_path,
})
})
.collect()
}
fn insert_message_creases(
editor: &mut Editor,
message_creases: &[MessageCrease],
window: &mut Window,
cx: &mut Context<'_, Editor>,
) -> Vec<CreaseId> {
let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let creases = message_creases
.iter()
@@ -1305,12 +1227,5 @@ pub fn insert_message_creases(
.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);
}
}
}
ids
}

View File

@@ -2,14 +2,17 @@ use std::{cmp::Reverse, sync::Arc};
use collections::IndexMap;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task};
use gpui::{
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
};
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId,
LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use ui::{ListItem, ListItemSpacing, prelude::*};
use ui::{KeyBinding, ListItem, ListItemSpacing, prelude::*};
use zed_actions::agent::OpenSettings;
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
@@ -20,6 +23,7 @@ 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,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<LanguageModelSelector>,
) -> LanguageModelSelector {
@@ -27,6 +31,7 @@ pub fn language_model_selector(
get_active_model,
on_model_changed,
popover_styles,
focus_handle,
window,
cx,
);
@@ -88,6 +93,7 @@ pub struct LanguageModelPickerDelegate {
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>,
popover_styles: bool,
focus_handle: FocusHandle,
}
impl LanguageModelPickerDelegate {
@@ -95,6 +101,7 @@ impl LanguageModelPickerDelegate {
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Self {
@@ -128,6 +135,7 @@ impl LanguageModelPickerDelegate {
},
)],
popover_styles,
focus_handle,
}
}
@@ -521,6 +529,8 @@ impl PickerDelegate for LanguageModelPickerDelegate {
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<gpui::AnyElement> {
let focus_handle = self.focus_handle.clone();
if !self.popover_styles {
return None;
}
@@ -528,22 +538,19 @@ impl PickerDelegate for LanguageModelPickerDelegate {
Some(
h_flex()
.w_full()
.p_1p5()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.p_1()
.gap_4()
.justify_between()
.child(
Button::new("configure", "Configure")
.icon(IconName::Settings)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.full_width()
.style(ButtonStyle::Outlined)
.key_binding(
KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
window.dispatch_action(OpenSettings.boxed_clone(), cx);
}),
)
.into_any(),

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
use crate::{
context::load_context,
context_store::ContextStore,
inline_prompt_editor::{
CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
},
@@ -73,8 +72,8 @@ impl TerminalInlineAssistant {
terminal_view: &Entity<TerminalView>,
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: Option<WeakEntity<HistoryStore>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
@@ -87,7 +86,6 @@ impl TerminalInlineAssistant {
cx,
)
});
let context_store = cx.new(|_cx| ContextStore::new(project));
let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
let prompt_editor = cx.new(|cx| {
@@ -97,10 +95,10 @@ impl TerminalInlineAssistant {
prompt_buffer.clone(),
codegen,
self.fs.clone(),
context_store.clone(),
workspace.clone(),
thread_store.clone(),
prompt_store.as_ref().map(|s| s.downgrade()),
prompt_store.clone(),
project.clone(),
workspace.clone(),
window,
cx,
)
@@ -119,8 +117,6 @@ impl TerminalInlineAssistant {
terminal_view,
prompt_editor,
workspace.clone(),
context_store,
prompt_store,
window,
cx,
);
@@ -227,6 +223,10 @@ impl TerminalInlineAssistant {
assist_id: TerminalInlineAssistId,
cx: &mut App,
) -> Result<Task<LanguageModelRequest>> {
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.context("No inline assistant model")?;
let assist = self.assists.get(&assist_id).context("invalid assist")?;
let shell = std::env::var("SHELL").ok();
@@ -243,45 +243,31 @@ impl TerminalInlineAssistant {
.ok()
.unwrap_or_default();
let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
&assist
.prompt_editor
.clone()
.context("invalid assist")?
.read(cx)
.prompt(cx),
&prompt_editor.read(cx).prompt(cx),
shell.as_deref(),
working_directory.as_deref(),
&latest_output,
)?;
let contexts = assist
.context_store
.read(cx)
.context()
.cloned()
.collect::<Vec<_>>();
let context_load_task = assist.workspace.update(cx, |workspace, cx| {
let project = workspace.project();
load_context(contexts, project, &assist.prompt_store, cx)
})?;
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
.inline_assistant_model()
.context("No inline assistant model")?;
let temperature = AgentSettings::temperature_for_model(&model, cx);
let mention_set = prompt_editor.read(cx).mention_set().clone();
let load_context_task = load_context(&mention_set, cx);
Ok(cx.background_spawn(async move {
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
reasoning_details: None,
};
context_load_task
.await
.add_to_request_message(&mut request_message);
if let Some(context) = load_context_task.await {
context.add_to_request_message(&mut request_message);
}
request_message.content.push(prompt.into());
@@ -409,8 +395,6 @@ struct TerminalInlineAssist {
prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
codegen: Entity<TerminalCodegen>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
_subscriptions: Vec<Subscription>,
}
@@ -420,8 +404,6 @@ impl TerminalInlineAssist {
terminal: &Entity<TerminalView>,
prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
workspace: WeakEntity<Workspace>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
window: &mut Window,
cx: &mut App,
) -> Self {
@@ -431,8 +413,6 @@ impl TerminalInlineAssist {
prompt_editor: Some(prompt_editor.clone()),
codegen: codegen.clone(),
workspace,
context_store,
prompt_store,
_subscriptions: vec![
window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
TerminalInlineAssistant::update_global(cx, |this, cx| {

View File

@@ -9,8 +9,8 @@ use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections
use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferSnapshot,
RowExt, ToOffset as _, ToPoint,
Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
@@ -22,11 +22,11 @@ use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
use futures::FutureExt;
use gpui::{
Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem,
Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point,
prelude::*, pulsating_between, size,
Action, Animation, AnimationExt, AnyElement, App, ClipboardEntry, ClipboardItem, Empty, Entity,
EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement,
ParentElement, Pixels, Render, RenderImage, SharedString, Size, StatefulInteractiveElement,
Styled, Subscription, Task, WeakEntity, actions, div, img, point, prelude::*,
pulsating_between, size,
};
use language::{
BufferSnapshot, LspAdapterDelegate, ToOffset,
@@ -66,7 +66,7 @@ use workspace::{
};
use workspace::{
Save, Toast, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
item::{self, FollowableItem, Item},
notifications::NotificationId,
pane,
searchable::{SearchEvent, SearchableItem},
@@ -280,6 +280,8 @@ impl TextThreadEditor {
.thought_process_output_sections()
.to_vec();
let slash_commands = text_thread.read(cx).slash_commands().clone();
let focus_handle = editor.read(cx).focus_handle(cx);
let mut this = Self {
text_thread,
slash_commands,
@@ -315,6 +317,7 @@ impl TextThreadEditor {
});
},
true, // Use popover styles for picker
focus_handle,
window,
cx,
)
@@ -390,7 +393,7 @@ impl TextThreadEditor {
let cursor = user_message
.start
.to_offset(self.text_thread.read(cx).buffer().read(cx));
cursor..cursor
MultiBufferOffset(cursor)..MultiBufferOffset(cursor)
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selections| {
@@ -431,7 +434,7 @@ impl TextThreadEditor {
let cursors = self.cursors(cx);
self.text_thread.update(cx, |text_thread, cx| {
let messages = text_thread
.messages_for_offsets(cursors, cx)
.messages_for_offsets(cursors.into_iter().map(|cursor| cursor.0), cx)
.into_iter()
.map(|message| message.id)
.collect();
@@ -439,9 +442,11 @@ impl TextThreadEditor {
});
}
fn cursors(&self, cx: &mut App) -> Vec<usize> {
fn cursors(&self, cx: &mut App) -> Vec<MultiBufferOffset> {
let selections = self.editor.update(cx, |editor, cx| {
editor.selections.all::<usize>(&editor.display_snapshot(cx))
editor
.selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
});
selections
.into_iter()
@@ -1580,7 +1585,11 @@ impl TextThreadEditor {
fn get_clipboard_contents(
&mut self,
cx: &mut Context<Self>,
) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
) -> (
String,
CopyMetadata,
Vec<text::Selection<MultiBufferOffset>>,
) {
let (mut selection, creases) = self.editor.update(cx, |editor, cx| {
let mut selection = editor
.selections
@@ -1638,30 +1647,26 @@ impl TextThreadEditor {
// If selection is empty, we want to copy the entire line
if selection.range().is_empty() {
let snapshot = text_thread.buffer().read(cx).snapshot();
let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
let point = snapshot.offset_to_point(selection.range().start);
selection.start = snapshot.point_to_offset(Point::new(point.row, 0));
selection.end = snapshot
.point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point()));
for chunk in text_thread
.buffer()
.read(cx)
.text_for_range(selection.range())
{
for chunk in snapshot.text_for_range(selection.range()) {
text.push_str(chunk);
}
} else {
for message in text_thread.messages(cx) {
if message.offset_range.start >= selection.range().end {
if message.offset_range.start >= selection.range().end.0 {
break;
} else if message.offset_range.end >= selection.range().start {
let range = cmp::max(message.offset_range.start, selection.range().start)
..cmp::min(message.offset_range.end, selection.range().end);
} else if message.offset_range.end >= selection.range().start.0 {
let range = cmp::max(message.offset_range.start, selection.range().start.0)
..cmp::min(message.offset_range.end, selection.range().end.0);
if !range.is_empty() {
for chunk in text_thread.buffer().read(cx).text_for_range(range) {
text.push_str(chunk);
}
if message.offset_range.end < selection.range().end {
if message.offset_range.end < selection.range().end.0 {
text.push('\n');
}
}
@@ -1743,7 +1748,7 @@ impl TextThreadEditor {
self.editor.update(cx, |editor, cx| {
let paste_position = editor
.selections
.newest::<usize>(&editor.display_snapshot(cx))
.newest::<MultiBufferOffset>(&editor.display_snapshot(cx))
.head();
editor.paste(action, window, cx);
@@ -1791,13 +1796,16 @@ impl TextThreadEditor {
editor.transact(window, cx, |editor, _window, cx| {
let edits = editor
.selections
.all::<usize>(&editor.display_snapshot(cx))
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
.into_iter()
.map(|selection| (selection.start..selection.end, "\n"));
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<usize>(&editor.display_snapshot(cx)) {
for selection in editor
.selections
.all::<MultiBufferOffset>(&editor.display_snapshot(cx))
{
image_positions.push(snapshot.anchor_before(selection.end));
}
});
@@ -1889,7 +1897,7 @@ impl TextThreadEditor {
let range = selection
.map(|endpoint| endpoint.to_offset(&buffer))
.range();
text_thread.split_message(range, cx);
text_thread.split_message(range.start.0..range.end.0, cx);
}
});
}
@@ -2583,11 +2591,11 @@ impl Item for TextThreadEditor {
type_id: TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<AnyView> {
) -> Option<gpui::AnyEntity> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
Some(self_handle.clone().into())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
Some(self.editor.clone().into())
} else {
None
}
@@ -2963,7 +2971,7 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
use editor::SelectionEffects;
use editor::{MultiBufferOffset, SelectionEffects};
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
@@ -3169,15 +3177,16 @@ mod tests {
text_thread: &Entity<TextThread>,
message_ix: usize,
cx: &mut TestAppContext,
) -> Range<usize> {
text_thread.update(cx, |text_thread, cx| {
) -> Range<MultiBufferOffset> {
let range = text_thread.update(cx, |text_thread, cx| {
text_thread
.messages(cx)
.nth(message_ix)
.unwrap()
.anchor_range
.to_offset(&text_thread.buffer().read(cx).snapshot())
})
});
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
}
fn assert_copy_paste_text_thread_editor<T: editor::ToOffset>(

View File

@@ -2,8 +2,8 @@ mod acp_onboarding_modal;
mod agent_notification;
mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
mod context_pill;
mod end_trial_upsell;
mod hold_for_default;
mod onboarding_modal;
mod unavailable_editing_tooltip;
mod usage_callout;
@@ -12,8 +12,8 @@ pub use acp_onboarding_modal::*;
pub use agent_notification::*;
pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
pub use context_pill::*;
pub use end_trial_upsell::*;
pub use hold_for_default::*;
pub use onboarding_modal::*;
pub use unavailable_editing_tooltip::*;
pub use usage_callout::*;

View File

@@ -1,858 +0,0 @@
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
use file_icons::FileIcons;
use futures::FutureExt as _;
use gpui::{
Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
pulsating_between,
};
use language_model::LanguageModelImage;
use project::Project;
use prompt_store::PromptStore;
use rope::Point;
use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
use util::paths::PathStyle;
use crate::context::{
AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
};
#[derive(IntoElement)]
pub enum ContextPill {
Added {
context: AddedContext,
dupe_name: bool,
focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
},
Suggested {
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,
focused: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
},
}
impl ContextPill {
pub fn added(
context: AddedContext,
dupe_name: bool,
focused: bool,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
) -> Self {
Self::Added {
context,
dupe_name,
on_remove,
focused,
on_click: None,
}
}
pub fn suggested(
name: SharedString,
icon_path: Option<SharedString>,
kind: ContextKind,
focused: bool,
) -> Self {
Self::Suggested {
name,
icon_path,
kind,
focused,
on_click: None,
}
}
pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
match &mut self {
ContextPill::Added { on_click, .. } => {
*on_click = Some(listener);
}
ContextPill::Suggested { on_click, .. } => {
*on_click = Some(listener);
}
}
self
}
pub fn id(&self) -> ElementId {
match self {
Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
Self::Suggested { .. } => "suggested-context-pill".into(),
}
}
pub fn icon(&self) -> Icon {
match self {
Self::Suggested {
icon_path: Some(icon_path),
..
} => Icon::from_path(icon_path),
Self::Suggested { kind, .. } => Icon::new(kind.icon()),
Self::Added { context, .. } => context.icon(),
}
}
}
impl RenderOnce for ContextPill {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let color = cx.theme().colors();
let base_pill = h_flex()
.id(self.id())
.pl_1()
.pb(px(1.))
.border_1()
.rounded_sm()
.gap_1()
.child(self.icon().size(IconSize::XSmall).color(Color::Muted));
match &self {
ContextPill::Added {
context,
dupe_name,
on_remove,
focused,
on_click,
} => {
let status_is_error = matches!(context.status, ContextStatus::Error { .. });
let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
base_pill
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
.map(|pill| {
if status_is_error {
pill.bg(cx.theme().status().error_background)
.border_color(cx.theme().status().error_border)
} else if status_is_warning {
pill.bg(cx.theme().status().warning_background)
.border_color(cx.theme().status().warning_border)
} else if *focused {
pill.bg(color.element_background)
.border_color(color.border_focused)
} else {
pill.bg(color.element_background)
.border_color(color.border.opacity(0.5))
}
})
.child(
h_flex()
.id("context-data")
.gap_1()
.child(
div().max_w_64().child(
Label::new(context.name.clone())
.size(LabelSize::Small)
.truncate(),
),
)
.when_some(context.parent.as_ref(), |element, parent_name| {
if *dupe_name {
element.child(
Label::new(parent_name.clone())
.size(LabelSize::XSmall)
.color(Color::Muted),
)
} else {
element
}
})
.when_some(context.tooltip.as_ref(), |element, tooltip| {
element.tooltip(Tooltip::text(tooltip.clone()))
})
.map(|element| match &context.status {
ContextStatus::Ready => element
.when_some(
context.render_hover.as_ref(),
|element, render_hover| {
let render_hover = render_hover.clone();
element.hoverable_tooltip(move |window, cx| {
render_hover(window, cx)
})
},
)
.into_any(),
ContextStatus::Loading { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.with_animation(
"pulsating-ctx-pill",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any_element(),
ContextStatus::Warning { message }
| ContextStatus::Error { message } => element
.tooltip(ui::Tooltip::text(message.clone()))
.into_any_element(),
}),
)
.when_some(on_remove.as_ref(), |element, on_remove| {
element.child(
IconButton::new(
context.handle.element_id("remove".into()),
IconName::Close,
)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.tooltip(Tooltip::text("Remove Context"))
.on_click({
let on_remove = on_remove.clone();
move |event, window, cx| on_remove(event, window, cx)
}),
)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.cursor_pointer().on_click(move |event, window, cx| {
on_click(event, window, cx);
cx.stop_propagation();
})
})
.into_any_element()
}
ContextPill::Suggested {
name,
icon_path: _,
kind: _,
focused,
on_click,
} => base_pill
.cursor_pointer()
.pr_1()
.border_dashed()
.map(|pill| {
if *focused {
pill.border_color(color.border_focused)
.bg(color.element_background.opacity(0.5))
} else {
pill.border_color(color.border)
}
})
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
.child(
div().max_w_64().child(
Label::new(name.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.truncate(),
),
)
.tooltip(|_window, cx| {
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
})
.when_some(on_click.as_ref(), |element, on_click| {
let on_click = on_click.clone();
element.on_click(move |event, window, cx| {
on_click(event, window, cx);
cx.stop_propagation();
})
})
.into_any(),
}
}
}
pub enum ContextStatus {
Ready,
Loading { message: SharedString },
Error { message: SharedString },
Warning { message: SharedString },
}
#[derive(RegisterComponent)]
pub struct AddedContext {
pub handle: AgentContextHandle,
pub kind: ContextKind,
pub name: SharedString,
pub parent: Option<SharedString>,
pub tooltip: Option<SharedString>,
pub icon_path: Option<SharedString>,
pub status: ContextStatus,
pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
}
impl AddedContext {
pub fn icon(&self) -> Icon {
match &self.status {
ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
_ => {
if let Some(icon_path) = &self.icon_path {
Icon::from_path(icon_path)
} else {
Icon::new(self.kind.icon())
}
}
}
}
/// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
/// `None` if `DirectoryContext` or `RulesContext` no longer exist.
///
/// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
pub fn new_pending(
handle: AgentContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
project: &Project,
model: Option<&Arc<dyn language_model::LanguageModel>>,
cx: &App,
) -> Option<AddedContext> {
match handle {
AgentContextHandle::File(handle) => {
Self::pending_file(handle, project.path_style(cx), cx)
}
AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
AgentContextHandle::Symbol(handle) => {
Self::pending_symbol(handle, project.path_style(cx), cx)
}
AgentContextHandle::Selection(handle) => {
Self::pending_selection(handle, project.path_style(cx), cx)
}
AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
AgentContextHandle::Image(handle) => {
Some(Self::image(handle, model, project.path_style(cx), cx))
}
}
}
fn pending_file(
handle: FileContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let full_path = handle
.buffer
.read(cx)
.file()?
.full_path(cx)
.to_string_lossy()
.to_string();
Some(Self::file(handle, &full_path, path_style, cx))
}
fn file(
handle: FileContextHandle,
full_path: &str,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
AddedContext {
kind: ContextKind::File,
name,
parent,
tooltip: Some(SharedString::new(full_path)),
icon_path: FileIcons::get_icon(Path::new(full_path), cx),
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::File(handle),
}
}
fn pending_directory(
handle: DirectoryContextHandle,
project: &Project,
cx: &App,
) -> Option<AddedContext> {
let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
let entry = worktree.entry_for_id(handle.entry_id)?;
let full_path = worktree
.full_path(&entry.path)
.to_string_lossy()
.to_string();
Some(Self::directory(handle, &full_path, project.path_style(cx)))
}
fn directory(
handle: DirectoryContextHandle,
full_path: &str,
path_style: PathStyle,
) -> AddedContext {
let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
AddedContext {
kind: ContextKind::Directory,
name,
parent,
tooltip: Some(SharedString::new(full_path)),
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::Directory(handle),
}
}
fn pending_symbol(
handle: SymbolContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.enclosing_line_range(cx),
path_style,
cx,
);
Some(AddedContext {
kind: ContextKind::Symbol,
name: handle.symbol.clone(),
parent: Some(excerpt.file_name_and_range.clone()),
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let handle = handle.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(handle.text(cx), cx).into()
}))
},
handle: AgentContextHandle::Symbol(handle),
})
}
fn pending_selection(
handle: SelectionContextHandle,
path_style: PathStyle,
cx: &App,
) -> Option<AddedContext> {
let excerpt = ContextFileExcerpt::new(
&handle.full_path(cx)?.to_string_lossy(),
handle.line_range(cx),
path_style,
cx,
);
Some(AddedContext {
kind: ContextKind::Selection,
name: excerpt.file_name_and_range.clone(),
parent: excerpt.parent_name.clone(),
tooltip: None,
icon_path: excerpt.icon_path.clone(),
status: ContextStatus::Ready,
render_hover: {
let handle = handle.clone();
Some(Rc::new(move |_, cx| {
excerpt.hover_view(handle.text(cx), cx).into()
}))
},
handle: AgentContextHandle::Selection(handle),
})
}
fn fetched_url(context: FetchedUrlContext) -> AddedContext {
AddedContext {
kind: ContextKind::FetchedUrl,
name: context.url.clone(),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::FetchedUrl(context),
}
}
fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
AddedContext {
kind: ContextKind::Thread,
name: handle.title(cx),
parent: None,
tooltip: None,
icon_path: None,
status: if handle.thread.read(cx).is_generating_summary() {
ContextStatus::Loading {
message: "Summarizing…".into(),
}
} else {
ContextStatus::Ready
},
render_hover: {
let thread = handle.thread.clone();
Some(Rc::new(move |_, cx| {
let text = thread
.update(cx, |thread, cx| thread.summary(cx))
.now_or_never()
.flatten()
.unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
ContextPillHover::new_text(text, cx).into()
}))
},
handle: AgentContextHandle::Thread(handle),
}
}
fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
AddedContext {
kind: ContextKind::TextThread,
name: handle.title(cx),
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: {
let text_thread = handle.text_thread.clone();
Some(Rc::new(move |_, cx| {
let text = text_thread.read(cx).to_xml(cx);
ContextPillHover::new_text(text.into(), cx).into()
}))
},
handle: AgentContextHandle::TextThread(handle),
}
}
fn pending_rules(
handle: RulesContextHandle,
prompt_store: Option<&Entity<PromptStore>>,
cx: &App,
) -> Option<AddedContext> {
let title = prompt_store
.as_ref()?
.read(cx)
.metadata(handle.prompt_id.into())?
.title
.unwrap_or_else(|| "Unnamed Rule".into());
Some(AddedContext {
kind: ContextKind::Rules,
name: title,
parent: None,
tooltip: None,
icon_path: None,
status: ContextStatus::Ready,
render_hover: None,
handle: AgentContextHandle::Rules(handle),
})
}
fn image(
context: ImageContext,
model: Option<&Arc<dyn language_model::LanguageModel>>,
path_style: PathStyle,
cx: &App,
) -> AddedContext {
let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
let (name, parent) =
extract_file_name_and_directory_from_full_path(full_path, path_style);
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
(name, parent, icon_path)
} else {
("Image".into(), None, None)
};
let status = match context.status(model) {
ImageStatus::Loading => ContextStatus::Loading {
message: "Loading…".into(),
},
ImageStatus::Error => ContextStatus::Error {
message: "Failed to load Image".into(),
},
ImageStatus::Warning => ContextStatus::Warning {
message: format!(
"{} doesn't support attaching Images as Context",
model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
)
.into(),
},
ImageStatus::Ready => ContextStatus::Ready,
};
AddedContext {
kind: ContextKind::Image,
name,
parent,
tooltip: None,
icon_path,
status,
render_hover: Some(Rc::new({
let image = context.original_image.clone();
move |_, cx| {
let image = image.clone();
ContextPillHover::new(cx, move |_, _| {
gpui::img(image.clone())
.max_w_96()
.max_h_96()
.into_any_element()
})
.into()
}
})),
handle: AgentContextHandle::Image(context),
}
}
}
fn extract_file_name_and_directory_from_full_path(
path: &str,
path_style: PathStyle,
) -> (SharedString, Option<SharedString>) {
let (parent, file_name) = path_style.split(path);
let parent = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
(SharedString::new(file_name), parent)
}
#[derive(Debug, Clone)]
struct ContextFileExcerpt {
pub file_name_and_range: SharedString,
pub full_path_and_range: SharedString,
pub parent_name: Option<SharedString>,
pub icon_path: Option<SharedString>,
}
impl ContextFileExcerpt {
pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
let (parent, file_name) = path_style.split(full_path);
let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
let mut full_path_and_range = full_path.to_owned();
full_path_and_range.push_str(&line_range_text);
let mut file_name_and_range = file_name.to_owned();
file_name_and_range.push_str(&line_range_text);
let parent_name = parent.and_then(|parent| {
let parent = parent.trim_end_matches(path_style.separator());
let (_, parent) = path_style.split(parent);
if parent.is_empty() {
None
} else {
Some(SharedString::new(parent))
}
});
let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
ContextFileExcerpt {
file_name_and_range: file_name_and_range.into(),
full_path_and_range: full_path_and_range.into(),
parent_name,
icon_path,
}
}
fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
let icon_path = self.icon_path.clone();
let full_path_and_range = self.full_path_and_range.clone();
ContextPillHover::new(cx, move |_, cx| {
v_flex()
.child(
h_flex()
.gap_0p5()
.w_full()
.max_w_full()
.border_b_1()
.border_color(cx.theme().colors().border.opacity(0.6))
.children(
icon_path
.clone()
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
)
.child(
// TODO: make this truncate on the left.
Label::new(full_path_and_range.clone())
.size(LabelSize::Small)
.ml_1(),
),
)
.child(
div()
.id("context-pill-hover-contents")
.overflow_scroll()
.max_w_128()
.max_h_96()
.child(Label::new(text.clone()).buffer_font(cx)),
)
.into_any_element()
})
}
}
struct ContextPillHover {
render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
}
impl ContextPillHover {
fn new(
cx: &mut App,
render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Entity<Self> {
cx.new(|_| Self {
render_hover: Box::new(render_hover),
})
}
fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
Self::new(cx, move |_, _| {
div()
.id("context-pill-hover-contents")
.overflow_scroll()
.max_w_128()
.max_h_96()
.child(content.clone())
.into_any_element()
})
}
}
impl Render for ContextPillHover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
tooltip_container(cx, move |this, cx| {
this.occlude()
.on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child((self.render_hover)(window, cx))
})
}
}
impl Component for AddedContext {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"AddedContext"
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let mut next_context_id = ContextId::zero();
let image_ready = (
"Ready",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
},
None,
PathStyle::local(),
cx,
),
);
let image_loading = (
"Loading",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: cx
.background_spawn(async move {
smol::Timer::after(Duration::from_secs(60 * 5)).await;
Some(LanguageModelImage::empty())
})
.shared(),
},
None,
PathStyle::local(),
cx,
),
);
let image_error = (
"Error",
AddedContext::image(
ImageContext {
context_id: next_context_id.post_inc(),
project_path: None,
full_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(None).shared(),
},
None,
PathStyle::local(),
cx,
),
);
Some(
v_flex()
.gap_6()
.children(
vec![image_ready, image_loading, image_error]
.into_iter()
.map(|(text, context)| {
single_example(
text,
ContextPill::added(context, false, false, None).into_any_element(),
)
}),
)
.into_any(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::App;
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
use std::sync::Arc;
#[gpui::test]
fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
assert!(!model.supports_images());
let image_context = ImageContext {
context_id: ContextId::zero(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
full_path: None,
};
let added_context =
AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
assert!(matches!(
added_context.status,
ContextStatus::Warning { .. }
));
assert!(matches!(added_context.kind, ContextKind::Image));
assert_eq!(added_context.name.as_ref(), "Image");
assert!(added_context.parent.is_none());
assert!(added_context.icon_path.is_none());
}
#[gpui::test]
fn test_image_context_ready_for_no_model(cx: &mut App) {
let image_context = ImageContext {
context_id: ContextId::zero(),
project_path: None,
original_image: Arc::new(Image::empty()),
image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
full_path: None,
};
let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
assert!(
matches!(added_context.status, ContextStatus::Ready),
"Expected ready status when no model provided"
);
assert!(matches!(added_context.kind, ContextKind::Image));
assert_eq!(added_context.name.as_ref(), "Image");
assert!(added_context.parent.is_none());
assert!(added_context.icon_path.is_none());
}
}

View File

@@ -0,0 +1,40 @@
use gpui::{App, IntoElement, Modifiers, RenderOnce, Window};
use ui::{prelude::*, render_modifiers};
#[derive(IntoElement)]
pub struct HoldForDefault {
is_default: bool,
}
impl HoldForDefault {
pub fn new(is_default: bool) -> Self {
Self { is_default }
}
}
impl RenderOnce for HoldForDefault {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(h_flex().flex_shrink_0().children(render_modifiers(
&Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(TextSize::Default.rems(cx).into()),
true,
)))
.child(div().map(|this| {
if self.is_default {
this.child("to unset as default")
} else {
this.child("to set as default")
}
}))
}
}

View File

@@ -67,6 +67,13 @@ pub enum Model {
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
ClaudeOpus4_5,
#[serde(
rename = "claude-opus-4-5-thinking",
alias = "claude-opus-4-5-thinking-latest"
)]
ClaudeOpus4_5Thinking,
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
ClaudeSonnet4,
#[serde(
@@ -131,6 +138,14 @@ impl Model {
}
pub fn from_id(id: &str) -> Result<Self> {
if id.starts_with("claude-opus-4-5-thinking") {
return Ok(Self::ClaudeOpus4_5Thinking);
}
if id.starts_with("claude-opus-4-5") {
return Ok(Self::ClaudeOpus4_5);
}
if id.starts_with("claude-opus-4-1-thinking") {
return Ok(Self::ClaudeOpus4_1Thinking);
}
@@ -208,6 +223,8 @@ impl Model {
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
Self::ClaudeOpus4_5 => "claude-opus-4-5-latest",
Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking-latest",
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
Self::ClaudeSonnet4_5 => "claude-sonnet-4-5-latest",
@@ -230,6 +247,7 @@ impl Model {
match self {
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
Self::ClaudeOpus4_5 | Self::ClaudeOpus4_5Thinking => "claude-opus-4-5-20251101",
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking => "claude-sonnet-4-5-20250929",
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
@@ -249,6 +267,8 @@ impl Model {
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
Self::ClaudeSonnet4 => "Claude Sonnet 4",
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
Self::ClaudeSonnet4_5 => "Claude Sonnet 4.5",
@@ -274,6 +294,8 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -303,6 +325,8 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -326,6 +350,8 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -348,6 +374,8 @@ impl Model {
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -372,6 +400,7 @@ impl Model {
match self {
Self::ClaudeOpus4
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_5
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4_5
| Self::Claude3_5Sonnet
@@ -383,6 +412,7 @@ impl Model {
| Self::Claude3Haiku => AnthropicModelMode::Default,
Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeHaiku4_5Thinking
@@ -393,13 +423,8 @@ impl Model {
}
}
pub const DEFAULT_BETA_HEADERS: &[&str] = &["prompt-caching-2024-07-31"];
pub fn beta_headers(&self) -> String {
let mut headers = Self::DEFAULT_BETA_HEADERS
.iter()
.map(|header| header.to_string())
.collect::<Vec<_>>();
pub fn beta_headers(&self) -> Option<String> {
let mut headers = vec![];
match self {
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => {
@@ -420,7 +445,11 @@ impl Model {
_ => {}
}
headers.join(",")
if headers.is_empty() {
None
} else {
Some(headers.join(","))
}
}
pub fn tool_model_id(&self) -> &str {
@@ -436,56 +465,12 @@ impl Model {
}
}
pub async fn complete(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
) -> Result<Response, AnthropicError> {
let uri = format!("{api_url}/v1/messages");
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.map_err(AnthropicError::BuildRequestBody)?;
let mut response = client
.send(request)
.await
.map_err(AnthropicError::HttpSend)?;
let status_code = response.status();
let mut body = String::new();
response
.body_mut()
.read_to_string(&mut body)
.await
.map_err(AnthropicError::ReadResponse)?;
if status_code.is_success() {
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
} else {
Err(AnthropicError::HttpResponseError {
status_code,
message: body,
})
}
}
pub async fn stream_completion(
client: &dyn HttpClient,
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
beta_headers: Option<String>,
) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> {
stream_completion_with_rate_limit_info(client, api_url, api_key, request, beta_headers)
.await
@@ -583,7 +568,7 @@ pub async fn stream_completion_with_rate_limit_info(
api_url: &str,
api_key: &str,
request: Request,
beta_headers: String,
beta_headers: Option<String>,
) -> Result<
(
BoxStream<'static, Result<Event, AnthropicError>>,
@@ -597,13 +582,17 @@ pub async fn stream_completion_with_rate_limit_info(
};
let uri = format!("{api_url}/v1/messages");
let request_builder = HttpRequest::builder()
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
.header("Anthropic-Version", "2023-06-01")
.header("Anthropic-Beta", beta_headers)
.header("X-Api-Key", api_key.trim())
.header("Content-Type", "application/json");
if let Some(beta_headers) = beta_headers {
request_builder = request_builder.header("Anthropic-Beta", beta_headers);
}
let serialized_request =
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
let request = request_builder

View File

@@ -250,7 +250,6 @@ impl PasswordProxy {
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
// todo(shell): There might be no powershell on the system
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
@@ -375,7 +374,7 @@ fn generate_askpass_script(
Ok(format!(
r#"
$ErrorActionPreference = 'Stop';
($args -join [char]0) | & {askpass_program} --askpass={askpass_socket} 2> $null
($args -join [char]0) | {askpass_program} --askpass={askpass_socket} 2> $null
"#,
))
}

View File

@@ -29,6 +29,7 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true

View File

@@ -880,10 +880,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
let num_sections = rng.random_range(0..=3);
let mut section_start = 0;
for _ in 0..num_sections {
let mut section_end = rng.random_range(section_start..=output_text.len());
while !output_text.is_char_boundary(section_end) {
section_end += 1;
}
let section_end = output_text.floor_char_boundary(
rng.random_range(section_start..=output_text.len()),
);
events.push(Ok(SlashCommandEvent::StartSection {
icon: IconName::Ai,
label: "section".into(),

View File

@@ -7,14 +7,16 @@ use assistant_slash_command::{
use assistant_slash_commands::FileCommandMetadata;
use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry};
use clock::ReplicaId;
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
use cloud_llm_client::{CompletionIntent, UsageLimit};
use collections::{HashMap, HashSet};
use fs::{Fs, RenameOptions};
use futures::{FutureExt, StreamExt, future::Shared};
use gpui::{
App, AppContext as _, Context, Entity, EventEmitter, RenderImage, SharedString, Subscription,
Task,
};
use itertools::Itertools as _;
use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, Point, ToOffset};
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
@@ -667,7 +669,7 @@ pub struct TextThread {
buffer: Entity<Buffer>,
pub(crate) parsed_slash_commands: Vec<ParsedSlashCommand>,
invoked_slash_commands: HashMap<InvokedSlashCommandId, InvokedSlashCommand>,
edits_since_last_parse: language::Subscription,
edits_since_last_parse: language::Subscription<usize>,
slash_commands: Arc<SlashCommandWorkingSet>,
pub(crate) slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
thought_process_output_sections: Vec<ThoughtProcessOutputSection<language::Anchor>>,
@@ -795,7 +797,7 @@ impl TextThread {
});
let message = MessageAnchor {
id: first_message_id,
start: language::Anchor::MIN,
start: language::Anchor::min_for_buffer(this.buffer.read(cx).remote_id()),
};
this.messages_metadata.insert(
first_message_id,
@@ -1145,12 +1147,10 @@ impl TextThread {
cx: &App,
) -> bool {
let version = &self.buffer.read(cx).version;
let observed_start = range.start == language::Anchor::MIN
|| range.start == language::Anchor::MAX
|| version.observed(range.start.timestamp);
let observed_end = range.end == language::Anchor::MIN
|| range.end == language::Anchor::MAX
|| version.observed(range.end.timestamp);
let observed_start =
range.start.is_min() || range.start.is_max() || version.observed(range.start.timestamp);
let observed_end =
range.end.is_min() || range.end.is_max() || version.observed(range.end.timestamp);
observed_start && observed_end
}
@@ -1416,6 +1416,7 @@ impl TextThread {
role: Role::User,
content: vec!["Respond only with OK, nothing else.".into()],
cache: false,
reasoning_details: None,
});
req
};
@@ -1851,14 +1852,17 @@ impl TextThread {
}
if ensure_trailing_newline
&& buffer.contains_str_at(command_range_end, "\n")
&& buffer
.chars_at(command_range_end)
.next()
.is_some_and(|c| c == '\n')
{
let newline_offset = insert_position.saturating_sub(1);
if buffer.contains_str_at(newline_offset, "\n")
if let Some((prev_char, '\n')) =
buffer.reversed_chars_at(insert_position).next_tuple()
&& last_section_range.is_none_or(|last_section_range| {
!last_section_range
.to_offset(buffer)
.contains(&newline_offset)
.contains(&(insert_position - prev_char.len_utf8()))
})
{
deletions.push((command_range_end..command_range_end + 1, ""));
@@ -2073,16 +2077,22 @@ impl TextThread {
});
match event {
LanguageModelCompletionEvent::StatusUpdate(status_update) => {
if let CompletionRequestStatus::UsageUpdated { amount, limit } = status_update {
this.update_model_request_usage(
amount as u32,
limit,
cx,
);
}
LanguageModelCompletionEvent::Started |
LanguageModelCompletionEvent::Queued {..} |
LanguageModelCompletionEvent::ToolUseLimitReached { .. } => {}
LanguageModelCompletionEvent::UsageUpdated { amount, limit } => {
this.update_model_request_usage(
amount as u32,
limit,
cx,
);
}
LanguageModelCompletionEvent::StartMessage { .. } => {}
LanguageModelCompletionEvent::ReasoningDetails(_) => {
// ReasoningDetails are metadata (signatures, encrypted data, format info)
// used for request/response validation, not UI content.
// The displayable thinking text is already handled by the Thinking event.
}
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
@@ -2306,6 +2316,7 @@ impl TextThread {
role: message.role,
content: Vec::new(),
cache: message.cache.as_ref().is_some_and(|cache| cache.is_anchor),
reasoning_details: None,
};
while let Some(content) = contents.peek() {
@@ -2677,6 +2688,7 @@ impl TextThread {
role: Role::User,
content: vec![SUMMARIZE_THREAD_PROMPT.into()],
cache: false,
reasoning_details: None,
});
// If there is no summary, it is set with `done: false` so that "Loading Summary…" can
@@ -2844,7 +2856,8 @@ impl TextThread {
messages.next();
}
}
let message_end_anchor = message_end.unwrap_or(language::Anchor::MAX);
let message_end_anchor =
message_end.unwrap_or(language::Anchor::max_for_buffer(buffer.remote_id()));
let message_end = message_end_anchor.to_offset(buffer);
return Some(Message {

View File

@@ -21,6 +21,7 @@ http_client.workspace = true
log.workspace = true
paths.workspace = true
release_channel.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true

View File

@@ -2,12 +2,13 @@ use anyhow::{Context as _, Result};
use client::Client;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
Task, Window, actions,
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, Window,
actions,
};
use http_client::{HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use semver::Version;
use serde::{Deserialize, Serialize};
use settings::{RegisterSetting, Settings, SettingsStore};
use smol::fs::File;
@@ -44,7 +45,7 @@ actions!(
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VersionCheckType {
Sha(AppCommitSha),
Semantic(SemanticVersion),
Semantic(Version),
}
#[derive(Serialize, Debug)]
@@ -100,7 +101,7 @@ impl AutoUpdateStatus {
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
current_version: Version,
client: Arc<Client>,
pending_poll: Option<Task<Option<()>>>,
quit_subscription: Option<gpui::Subscription>,
@@ -256,7 +257,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
match release_channel {
ReleaseChannel::Stable | ReleaseChannel::Preview => {
let auto_updater = auto_updater.read(cx);
let current_version = auto_updater.current_version;
let current_version = auto_updater.current_version.clone();
let release_channel = release_channel.dev_name();
let path = format!("/releases/{release_channel}/{current_version}");
let url = &auto_updater.client.http_client().build_url(&path);
@@ -322,7 +323,7 @@ impl AutoUpdater {
cx.default_global::<GlobalAutoUpdate>().0.clone()
}
fn new(current_version: SemanticVersion, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
fn new(current_version: Version, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
// On windows, executable files cannot be overwritten while they are
// running, so we must wait to overwrite the application until quitting
// or restarting. When quitting the app, we spawn the auto update helper
@@ -400,8 +401,8 @@ impl AutoUpdater {
}));
}
pub fn current_version(&self) -> SemanticVersion {
self.current_version
pub fn current_version(&self) -> Version {
self.current_version.clone()
}
pub fn status(&self) -> AutoUpdateStatus {
@@ -422,7 +423,7 @@ impl AutoUpdater {
// Ok(None).
pub async fn download_remote_server_release(
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
version: Option<Version>,
os: &str,
arch: &str,
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
@@ -469,7 +470,7 @@ impl AutoUpdater {
pub async fn get_remote_server_release_url(
channel: ReleaseChannel,
version: Option<SemanticVersion>,
version: Option<Version>,
os: &str,
arch: &str,
cx: &mut AsyncApp,
@@ -491,7 +492,7 @@ impl AutoUpdater {
async fn get_release_asset(
this: &Entity<Self>,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
version: Option<Version>,
asset: &str,
os: &str,
arch: &str,
@@ -554,7 +555,7 @@ impl AutoUpdater {
this.read_with(cx, |this, cx| {
(
this.client.http_client(),
this.current_version,
this.current_version.clone(),
this.status.clone(),
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
)
@@ -627,16 +628,19 @@ impl AutoUpdater {
fn check_if_fetched_version_is_newer(
release_channel: ReleaseChannel,
app_commit_sha: Result<Option<String>>,
installed_version: SemanticVersion,
installed_version: Version,
fetched_version: String,
status: AutoUpdateStatus,
) -> Result<Option<VersionCheckType>> {
let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
let parsed_fetched_version = fetched_version.parse::<Version>();
if let AutoUpdateStatus::Updated { version, .. } = status {
match version {
VersionCheckType::Sha(cached_version) => {
let should_download = fetched_version != cached_version.full();
let should_download = parsed_fetched_version
.as_ref()
.ok()
.is_none_or(|version| version.build.as_str() != cached_version.full());
let newer_version = should_download
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
return Ok(newer_version);
@@ -655,7 +659,12 @@ impl AutoUpdater {
let should_download = app_commit_sha
.ok()
.flatten()
.map(|sha| fetched_version != sha)
.map(|sha| {
parsed_fetched_version
.as_ref()
.ok()
.is_none_or(|version| version.build.as_str() != sha)
})
.unwrap_or(true);
let newer_version = should_download
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
@@ -708,8 +717,8 @@ impl AutoUpdater {
}
fn check_if_fetched_version_is_newer_non_nightly(
installed_version: SemanticVersion,
fetched_version: SemanticVersion,
installed_version: Version,
fetched_version: Version,
) -> Result<Option<VersionCheckType>> {
let should_download = fetched_version > installed_version;
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
@@ -1020,7 +1029,7 @@ mod tests {
cx.update(|cx| {
settings::init(cx);
let current_version = SemanticVersion::new(0, 100, 0);
let current_version = semver::Version::new(0, 100, 0);
release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
let clock = Arc::new(FakeSystemClock::new());
@@ -1059,7 +1068,7 @@ mod tests {
auto_updater.read_with(cx, |updater, _| {
assert_eq!(updater.status(), AutoUpdateStatus::Idle);
assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0));
assert_eq!(updater.current_version(), semver::Version::new(0, 100, 0));
});
release_available.store(true, atomic::Ordering::SeqCst);
@@ -1078,7 +1087,7 @@ mod tests {
assert_eq!(
status,
AutoUpdateStatus::Downloading {
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
}
);
@@ -1108,7 +1117,7 @@ mod tests {
assert_eq!(
status,
AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
}
);
let will_restart = cx.expect_restart();
@@ -1122,9 +1131,9 @@ mod tests {
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = SemanticVersion::new(1, 0, 0);
let fetched_version = semver::Version::new(1, 0, 0);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1141,9 +1150,9 @@ mod tests {
fn test_stable_does_update_when_fetched_version_is_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = SemanticVersion::new(1, 0, 1);
let fetched_version = semver::Version::new(1, 0, 1);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1163,11 +1172,11 @@ mod tests {
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
};
let fetched_version = SemanticVersion::new(1, 0, 1);
let fetched_version = semver::Version::new(1, 0, 1);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1184,11 +1193,11 @@ mod tests {
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
};
let fetched_version = SemanticVersion::new(1, 0, 2);
let fetched_version = semver::Version::new(1, 0, 2);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1208,9 +1217,10 @@ mod tests {
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let mut installed_version = semver::Version::new(1, 0, 0);
installed_version.build = semver::BuildMetadata::new("a").unwrap();
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
let fetched_sha = "1.0.0+a".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1227,7 +1237,7 @@ mod tests {
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "b".to_string();
@@ -1246,14 +1256,15 @@ mod tests {
}
#[test]
fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
fn test_nightly_does_not_update_when_fetched_version_is_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let mut installed_version = semver::Version::new(1, 0, 0);
installed_version.build = semver::BuildMetadata::new("a").unwrap();
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "b".to_string();
let fetched_sha = "1.0.0+b".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1270,11 +1281,12 @@ mod tests {
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = SemanticVersion::new(1, 0, 0);
let mut installed_version = semver::Version::new(1, 0, 0);
installed_version.build = semver::BuildMetadata::new("a").unwrap();
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "c".to_string();
let fetched_sha = "1.0.0+c".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1294,7 +1306,7 @@ mod tests {
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
@@ -1317,11 +1329,11 @@ mod tests {
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "b".to_string();
let fetched_sha = "1.0.0+b".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
@@ -1339,7 +1351,7 @@ mod tests {
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = SemanticVersion::new(1, 0, 0);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};

View File

@@ -51,6 +51,13 @@ pub enum Model {
alias = "claude-opus-4-1-thinking-latest"
)]
ClaudeOpus4_1Thinking,
#[serde(rename = "claude-opus-4-5", alias = "claude-opus-4-5-latest")]
ClaudeOpus4_5,
#[serde(
rename = "claude-opus-4-5-thinking",
alias = "claude-opus-4-5-thinking-latest"
)]
ClaudeOpus4_5Thinking,
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
Claude3_5SonnetV2,
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
@@ -141,7 +148,19 @@ impl Model {
}
pub fn from_id(id: &str) -> anyhow::Result<Self> {
if id.starts_with("claude-3-5-sonnet-v2") {
if id.starts_with("claude-opus-4-5-thinking") {
Ok(Self::ClaudeOpus4_5Thinking)
} else if id.starts_with("claude-opus-4-5") {
Ok(Self::ClaudeOpus4_5)
} else if id.starts_with("claude-opus-4-1-thinking") {
Ok(Self::ClaudeOpus4_1Thinking)
} else if id.starts_with("claude-opus-4-1") {
Ok(Self::ClaudeOpus4_1)
} else if id.starts_with("claude-opus-4-thinking") {
Ok(Self::ClaudeOpus4Thinking)
} else if id.starts_with("claude-opus-4") {
Ok(Self::ClaudeOpus4)
} else if id.starts_with("claude-3-5-sonnet-v2") {
Ok(Self::Claude3_5SonnetV2)
} else if id.starts_with("claude-3-opus") {
Ok(Self::Claude3Opus)
@@ -178,6 +197,8 @@ impl Model {
Model::ClaudeOpus4_1 => "claude-opus-4-1",
Model::ClaudeOpus4Thinking => "claude-opus-4-thinking",
Model::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking",
Model::ClaudeOpus4_5 => "claude-opus-4-5",
Model::ClaudeOpus4_5Thinking => "claude-opus-4-5-thinking",
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
Model::Claude3Opus => "claude-3-opus",
@@ -245,6 +266,9 @@ impl Model {
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
"anthropic.claude-opus-4-1-20250805-v1:0"
}
Model::ClaudeOpus4_5 | Model::ClaudeOpus4_5Thinking => {
"anthropic.claude-opus-4-5-20251101-v1:0"
}
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
@@ -309,6 +333,8 @@ impl Model {
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
Self::ClaudeOpus4_5 => "Claude Opus 4.5",
Self::ClaudeOpus4_5Thinking => "Claude Opus 4.5 Thinking",
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
Self::Claude3Opus => "Claude 3 Opus",
@@ -379,7 +405,9 @@ impl Model {
| Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1Thinking => 200_000,
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => 200_000,
Self::AmazonNovaPremier => 1_000_000,
Self::PalmyraWriterX5 => 1_000_000,
Self::PalmyraWriterX4 => 128_000,
@@ -393,7 +421,11 @@ impl Model {
Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3_5Haiku => 4_096,
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => 128_000,
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => 64_000,
Self::ClaudeSonnet4_5 | Self::ClaudeSonnet4_5Thinking | Self::ClaudeHaiku4_5 => 64_000,
Self::ClaudeSonnet4_5
| Self::ClaudeSonnet4_5Thinking
| Self::ClaudeHaiku4_5
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => 64_000,
Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
@@ -418,6 +450,8 @@ impl Model {
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -443,6 +477,8 @@ impl Model {
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking
| Self::ClaudeSonnet4
| Self::ClaudeSonnet4Thinking
| Self::ClaudeSonnet4_5
@@ -484,7 +520,9 @@ impl Model {
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking => true,
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => true,
// Custom models - check if they have cache configuration
Self::Custom {
@@ -506,7 +544,9 @@ impl Model {
| Self::ClaudeOpus4
| Self::ClaudeOpus4Thinking
| Self::ClaudeOpus4_1
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
| Self::ClaudeOpus4_1Thinking
| Self::ClaudeOpus4_5
| Self::ClaudeOpus4_5Thinking => Some(BedrockModelCacheConfiguration {
max_cache_anchors: 4,
min_total_token: 1024,
}),
@@ -535,11 +575,11 @@ impl Model {
budget_tokens: Some(4096),
}
}
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
BedrockModelMode::Thinking {
budget_tokens: Some(4096),
}
}
Model::ClaudeOpus4Thinking
| Model::ClaudeOpus4_1Thinking
| Model::ClaudeOpus4_5Thinking => BedrockModelMode::Thinking {
budget_tokens: Some(4096),
},
_ => BedrockModelMode::Default,
}
}
@@ -593,6 +633,8 @@ impl Model {
| Model::ClaudeOpus4Thinking
| Model::ClaudeOpus4_1
| Model::ClaudeOpus4_1Thinking
| Model::ClaudeOpus4_5
| Model::ClaudeOpus4_5Thinking
| Model::Claude3Haiku
| Model::Claude3Opus
| Model::Claude3Sonnet

View File

@@ -123,7 +123,7 @@ impl Render for Breadcrumbs {
.upgrade()
.zip(zed_actions::outline::TOGGLE_OUTLINE.get())
{
callback(editor.to_any(), window, cx);
callback(editor.to_any_view(), window, cx);
}
}
})

View File

@@ -147,11 +147,16 @@ impl std::fmt::Debug for BufferDiffInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferDiffSnapshot")
.field("hunks", &self.hunks)
.field("remote_id", &self.base_text.remote_id())
.finish()
}
}
impl BufferDiffSnapshot {
pub fn buffer_diff_id(&self) -> BufferId {
self.inner.base_text.remote_id()
}
fn empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffSnapshot {
BufferDiffSnapshot {
inner: BufferDiffInner {
@@ -339,7 +344,7 @@ impl BufferDiffInner {
};
let hunk = PendingHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
diff_base_byte_range: 0..index_text.map_or(0, |rope| rope.len()),
buffer_version: buffer.version().clone(),
new_status,
@@ -779,7 +784,7 @@ fn compute_hunks(
} else {
tree.push(
InternalDiffHunk {
buffer_range: Anchor::MIN..Anchor::MAX,
buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
diff_base_byte_range: 0..0,
},
&buffer,
@@ -940,10 +945,10 @@ impl BufferDiff {
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
if self.secondary_diff.is_some() {
self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
buffer_range: Anchor::MIN..Anchor::MIN,
buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
});
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::MIN..Anchor::MAX),
changed_range: Some(Anchor::min_max_range_for_buffer(self.buffer_id)),
});
}
}
@@ -1064,7 +1069,10 @@ impl BufferDiff {
{
(false, new_state.compare(state, buffer))
}
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
_ => (
true,
Some(text::Anchor::min_max_range_for_buffer(self.buffer_id)),
),
};
if let Some(secondary_changed_range) = secondary_diff_change
@@ -1125,7 +1133,11 @@ impl BufferDiff {
buffer_snapshot: &'a text::BufferSnapshot,
cx: &'a App,
) -> impl 'a + Iterator<Item = DiffHunk> {
self.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer_snapshot, cx)
self.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer_snapshot.remote_id()),
buffer_snapshot,
cx,
)
}
pub fn hunks_intersecting_range<'a>(
@@ -1221,7 +1233,9 @@ impl BufferDiff {
impl DiffHunk {
pub fn is_created_file(&self) -> bool {
self.diff_base_byte_range == (0..0) && self.buffer_range == (Anchor::MIN..Anchor::MAX)
self.diff_base_byte_range == (0..0)
&& self.buffer_range.start.is_min()
&& self.buffer_range.end.is_min()
}
pub fn status(&self) -> DiffHunkStatus {
@@ -1388,7 +1402,10 @@ mod tests {
let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
assert_hunks(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
&buffer,
),
&buffer,
&diff_base,
&[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
@@ -1397,7 +1414,10 @@ mod tests {
buffer.edit([(0..0, "point five\n")]);
diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
assert_hunks(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
&buffer,
),
&buffer,
&diff_base,
&[
@@ -1408,7 +1428,10 @@ mod tests {
diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
assert_hunks::<&str, _>(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
&buffer,
),
&buffer,
&diff_base,
&[],
@@ -1482,7 +1505,10 @@ mod tests {
];
assert_hunks(
uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
uncommitted_diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
&buffer,
),
&buffer,
&head_text,
&expected_hunks,
@@ -1541,8 +1567,11 @@ mod tests {
})
.await;
assert_eq!(
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer)
.count(),
diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(buffer.remote_id()),
&buffer
)
.count(),
8
);
@@ -2154,8 +2183,12 @@ mod tests {
let mut diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
let mut hunks = diff.update(cx, |diff, cx| {
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
.collect::<Vec<_>>()
diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(diff.buffer_id),
&working_copy,
cx,
)
.collect::<Vec<_>>()
});
if hunks.is_empty() {
return;
@@ -2184,8 +2217,12 @@ mod tests {
diff = uncommitted_diff(&working_copy, &index_text, head_text.clone(), cx);
let found_hunks = diff.update(cx, |diff, cx| {
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &working_copy, cx)
.collect::<Vec<_>>()
diff.hunks_intersecting_range(
Anchor::min_max_range_for_buffer(diff.buffer_id),
&working_copy,
cx,
)
.collect::<Vec<_>>()
});
assert_eq!(hunks.len(), found_hunks.len());

View File

@@ -37,6 +37,7 @@ collections = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
rpc = { workspace = true, features = ["test-support"] }
client = { workspace = true, features = ["test-support"] }
semver.workspace = true
settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }

View File

@@ -1,7 +1,7 @@
use super::*;
use client::{Client, UserStore};
use clock::FakeSystemClock;
use gpui::{App, AppContext as _, Entity, SemanticVersion};
use gpui::{App, AppContext as _, Entity};
use http_client::FakeHttpClient;
use rpc::proto::{self};
use settings::SettingsStore;
@@ -236,7 +236,7 @@ fn test_dangling_channel_paths(cx: &mut App) {
fn init_test(cx: &mut App) -> Entity<ChannelStore> {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
release_channel::init(SemanticVersion::default(), cx);
release_channel::init(semver::Version::new(0, 0, 0), cx);
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();

View File

@@ -34,6 +34,10 @@ util.workspace = true
tempfile.workspace = true
rayon.workspace = true
[dev-dependencies]
serde_json.workspace = true
util = { workspace = true, features = ["test-support"] }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
exec.workspace = true
fork.workspace = true

View File

@@ -23,4 +23,7 @@ fn main() {
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
}
if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") {
println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}");
}
}

View File

@@ -12,7 +12,9 @@ use clap::Parser;
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
use parking_lot::Mutex;
use std::{
env, fs, io,
env,
ffi::OsStr,
fs, io,
path::{Path, PathBuf},
process::ExitStatus,
sync::Arc,
@@ -129,37 +131,177 @@ struct Args {
askpass: Option<String>,
}
/// Parses a path containing a position (e.g. `path:line:column`)
/// and returns its canonicalized string representation.
///
/// If a part of path doesn't exist, it will canonicalize the
/// existing part and append the non-existing part.
///
/// This method must return an absolute path, as many zed
/// crates assume absolute paths.
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
let canonicalized = match Path::new(argument_str).canonicalize() {
Ok(existing_path) => PathWithPosition::from_path(existing_path),
Err(_) => {
let path = PathWithPosition::parse_str(argument_str);
match Path::new(argument_str).canonicalize() {
Ok(existing_path) => Ok(PathWithPosition::from_path(existing_path)),
Err(_) => PathWithPosition::parse_str(argument_str).map_path(|mut path| {
let curdir = env::current_dir().context("retrieving current directory")?;
path.map_path(|path| match fs::canonicalize(&path) {
Ok(path) => Ok(path),
Err(e) => {
if let Some(mut parent) = path.parent() {
if parent == Path::new("") {
parent = &curdir
}
match fs::canonicalize(parent) {
Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
Err(_) => Err(e),
}
} else {
Err(e)
}
let mut children = Vec::new();
let root;
loop {
// canonicalize handles './', and '/'.
if let Ok(canonicalized) = fs::canonicalize(&path) {
root = canonicalized;
break;
}
})
}
.with_context(|| format!("parsing as path with position {argument_str}"))?,
};
Ok(canonicalized.to_string(|path| path.to_string_lossy().into_owned()))
// The comparison to `curdir` is just a shortcut
// since we know it is canonical. The other one
// is if `argument_str` is a string that starts
// with a name (e.g. "foo/bar").
if path == curdir || path == Path::new("") {
root = curdir;
break;
}
children.push(
path.file_name()
.with_context(|| format!("parsing as path with position {argument_str}"))?
.to_owned(),
);
if !path.pop() {
unreachable!("parsing as path with position {argument_str}");
}
}
Ok(children.iter().rev().fold(root, |mut path, child| {
path.push(child);
path
}))
}),
}
.map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use util::path;
use util::paths::SanitizedPath;
use util::test::TempTree;
macro_rules! assert_path_eq {
($left:expr, $right:expr) => {
assert_eq!(
SanitizedPath::new(Path::new(&$left)),
SanitizedPath::new(Path::new(&$right))
)
};
}
fn cwd() -> PathBuf {
env::current_dir().unwrap()
}
static CWD_LOCK: Mutex<()> = Mutex::new(());
fn with_cwd<T>(path: &Path, f: impl FnOnce() -> anyhow::Result<T>) -> anyhow::Result<T> {
let _lock = CWD_LOCK.lock();
let old_cwd = cwd();
env::set_current_dir(path)?;
let result = f();
env::set_current_dir(old_cwd)?;
result
}
#[test]
fn test_parse_non_existing_path() {
// Absolute path
let result = parse_path_with_position(path!("/non/existing/path.txt")).unwrap();
assert_path_eq!(result, path!("/non/existing/path.txt"));
// Absolute path in cwd
let path = cwd().join(path!("non/existing/path.txt"));
let expected = path.to_string_lossy().to_string();
let result = parse_path_with_position(&expected).unwrap();
assert_path_eq!(result, expected);
// Relative path
let result = parse_path_with_position(path!("non/existing/path.txt")).unwrap();
assert_path_eq!(result, expected)
}
#[test]
fn test_parse_existing_path() {
let temp_tree = TempTree::new(json!({
"file.txt": "",
}));
let file_path = temp_tree.path().join("file.txt");
let expected = file_path.to_string_lossy().to_string();
// Absolute path
let result = parse_path_with_position(file_path.to_str().unwrap()).unwrap();
assert_path_eq!(result, expected);
// Relative path
let result = with_cwd(temp_tree.path(), || parse_path_with_position("file.txt")).unwrap();
assert_path_eq!(result, expected);
}
// NOTE:
// While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus
// we assume that they are not supported out of the box.
#[cfg(not(windows))]
#[test]
fn test_parse_symlink_file() {
let temp_tree = TempTree::new(json!({
"target.txt": "",
}));
let target_path = temp_tree.path().join("target.txt");
let symlink_path = temp_tree.path().join("symlink.txt");
std::os::unix::fs::symlink(&target_path, &symlink_path).unwrap();
// Absolute path
let result = parse_path_with_position(symlink_path.to_str().unwrap()).unwrap();
assert_eq!(result, target_path.to_string_lossy());
// Relative path
let result =
with_cwd(temp_tree.path(), || parse_path_with_position("symlink.txt")).unwrap();
assert_eq!(result, target_path.to_string_lossy());
}
#[cfg(not(windows))]
#[test]
fn test_parse_symlink_dir() {
let temp_tree = TempTree::new(json!({
"some": {
"dir": { // symlink target
"ec": {
"tory": {
"file.txt": "",
}}}}}));
let target_file_path = temp_tree.path().join("some/dir/ec/tory/file.txt");
let expected = target_file_path.to_string_lossy();
let dir_path = temp_tree.path().join("some/dir");
let symlink_path = temp_tree.path().join("symlink");
std::os::unix::fs::symlink(&dir_path, &symlink_path).unwrap();
// Absolute path
let result =
parse_path_with_position(symlink_path.join("ec/tory/file.txt").to_str().unwrap())
.unwrap();
assert_eq!(result, expected);
// Relative path
let result = with_cwd(temp_tree.path(), || {
parse_path_with_position("symlink/ec/tory/file.txt")
})
.unwrap();
assert_eq!(result, expected);
}
}
fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
let mut source = PathWithPosition::parse_str(source);
let mut command = util::command::new_std_command("wsl.exe");
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
if user.is_empty() {
@@ -170,22 +312,35 @@ fn parse_path_in_wsl(source: &str, wsl: &str) -> Result<String> {
(None, wsl)
};
let mut args = vec!["--distribution", distro_name];
if let Some(user) = user {
command.arg("--user").arg(user);
args.push("--user");
args.push(user);
}
let output = command
.arg("--distribution")
.arg(distro_name)
.arg("--exec")
.arg("wslpath")
.arg("-m")
.arg(&source.path)
.output()?;
let command = [
OsStr::new("realpath"),
OsStr::new("-s"),
source.path.as_ref(),
];
let result = String::from_utf8_lossy(&output.stdout);
let prefix = format!("//wsl.localhost/{}", distro_name);
source.path = Path::new(result.trim().strip_prefix(&prefix).unwrap_or(&result)).to_owned();
let output = util::command::new_std_command("wsl.exe")
.args(&args)
.arg("--exec")
.args(&command)
.output()?;
let result = if output.status.success() {
String::from_utf8_lossy(&output.stdout).to_string()
} else {
let fallback = util::command::new_std_command("wsl.exe")
.args(&args)
.arg("--")
.args(&command)
.output()?;
String::from_utf8_lossy(&fallback.stdout).to_string()
};
source.path = Path::new(result.trim()).to_owned();
Ok(source.to_string(|path| path.to_string_lossy().into_owned()))
}

View File

@@ -70,6 +70,7 @@ settings = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.dependencies]
semver.workspace = true
windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]

View File

@@ -158,7 +158,7 @@ pub fn os_version() -> String {
let mut info = unsafe { std::mem::zeroed() };
let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut info) };
if status.is_ok() {
gpui::SemanticVersion::new(
semver::Version::new(
info.dwMajorVersion as _,
info.dwMinorVersion as _,
info.dwBuildNumber as _,

View File

@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Write as _},
ops::{Add, Range, Sub},
path::{Path, PathBuf},
path::Path,
sync::Arc,
};
use strum::EnumIter;
@@ -17,7 +17,7 @@ pub struct PlanContextRetrievalRequest {
pub excerpt_path: Arc<Path>,
pub excerpt_line_range: Range<Line>,
pub cursor_file_max_row: Line,
pub events: Vec<Event>,
pub events: Vec<Arc<Event>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -36,7 +36,7 @@ pub struct PredictEditsRequest {
pub signatures: Vec<Signature>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub referenced_declarations: Vec<ReferencedDeclaration>,
pub events: Vec<Event>,
pub events: Vec<Arc<Event>>,
#[serde(default)]
pub can_collect_data: bool,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
@@ -80,6 +80,8 @@ pub enum PromptFormat {
Minimal,
/// One-sentence instructions + FIM-like template
MinimalQwen,
/// No instructions, Qwen chat + Seed-Coder 1120 FIM-like template
SeedCoder1120,
}
impl PromptFormat {
@@ -108,6 +110,7 @@ impl std::fmt::Display for PromptFormat {
PromptFormat::OldTextNewText => write!(f, "Old Text / New Text"),
PromptFormat::Minimal => write!(f, "Minimal"),
PromptFormat::MinimalQwen => write!(f, "Minimal + Qwen FIM"),
PromptFormat::SeedCoder1120 => write!(f, "Seed-Coder 1120"),
}
}
}
@@ -117,10 +120,11 @@ impl std::fmt::Display for PromptFormat {
#[serde(tag = "event")]
pub enum Event {
BufferChange {
path: Option<PathBuf>,
old_path: Option<PathBuf>,
path: Arc<Path>,
old_path: Arc<Path>,
diff: String,
predicted: bool,
in_open_source_repo: bool,
},
}
@@ -132,23 +136,21 @@ impl Display for Event {
old_path,
diff,
predicted,
..
} => {
let new_path = path.as_deref().unwrap_or(Path::new("untitled"));
let old_path = old_path.as_deref().unwrap_or(new_path);
if *predicted {
write!(
f,
"// User accepted prediction:\n--- a/{}\n+++ b/{}\n{diff}",
DiffPathFmt(old_path),
DiffPathFmt(new_path)
DiffPathFmt(path)
)
} else {
write!(
f,
"--- a/{}\n+++ b/{}\n{diff}",
DiffPathFmt(old_path),
DiffPathFmt(new_path)
DiffPathFmt(path)
)
}
}
@@ -297,10 +299,11 @@ mod tests {
#[test]
fn test_event_display() {
let ev = Event::BufferChange {
path: None,
old_path: None,
path: Path::new("untitled").into(),
old_path: Path::new("untitled").into(),
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
predicted: false,
in_open_source_repo: true,
};
assert_eq!(
ev.to_string(),
@@ -314,10 +317,11 @@ mod tests {
);
let ev = Event::BufferChange {
path: Some(PathBuf::from("foo/bar.txt")),
old_path: Some(PathBuf::from("foo/bar.txt")),
path: Path::new("foo/bar.txt").into(),
old_path: Path::new("foo/bar.txt").into(),
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
predicted: false,
in_open_source_repo: true,
};
assert_eq!(
ev.to_string(),
@@ -331,10 +335,11 @@ mod tests {
);
let ev = Event::BufferChange {
path: Some(PathBuf::from("abc.txt")),
old_path: Some(PathBuf::from("123.txt")),
path: Path::new("abc.txt").into(),
old_path: Path::new("123.txt").into(),
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
predicted: false,
in_open_source_repo: true,
};
assert_eq!(
ev.to_string(),
@@ -348,10 +353,11 @@ mod tests {
);
let ev = Event::BufferChange {
path: Some(PathBuf::from("abc.txt")),
old_path: Some(PathBuf::from("123.txt")),
path: Path::new("abc.txt").into(),
old_path: Path::new("123.txt").into(),
diff: "@@ -1,2 +1,2 @@\n-a\n-b\n".into(),
predicted: true,
in_open_source_repo: true,
};
assert_eq!(
ev.to_string(),

View File

@@ -169,15 +169,18 @@ pub fn build_prompt(
) -> Result<(String, SectionLabels)> {
let mut section_labels = Default::default();
let prompt_data = PromptData {
events: request.events.clone(),
cursor_point: request.cursor_point,
cursor_path: request.excerpt_path.clone(),
included_files: request.included_files.clone(),
};
match request.prompt_format {
PromptFormat::MinimalQwen => {
let prompt = MinimalQwenPrompt {
events: request.events.clone(),
cursor_point: request.cursor_point,
cursor_path: request.excerpt_path.clone(),
included_files: request.included_files.clone(),
};
return Ok((prompt.render(), section_labels));
return Ok((MinimalQwenPrompt.render(&prompt_data), section_labels));
}
PromptFormat::SeedCoder1120 => {
return Ok((SeedCoder1120Prompt.render(&prompt_data), section_labels));
}
_ => (),
};
@@ -208,6 +211,7 @@ pub fn build_prompt(
}
PromptFormat::OnlySnippets => vec![],
PromptFormat::MinimalQwen => unreachable!(),
PromptFormat::SeedCoder1120 => unreachable!(),
};
let mut prompt = match request.prompt_format {
@@ -218,6 +222,7 @@ pub fn build_prompt(
PromptFormat::OnlySnippets => String::new(),
PromptFormat::Minimal => STUDENT_MODEL_INSTRUCTIONS.to_string(),
PromptFormat::MinimalQwen => unreachable!(),
PromptFormat::SeedCoder1120 => unreachable!(),
};
if request.events.is_empty() {
@@ -328,6 +333,13 @@ pub fn build_prompt(
Ok((prompt, section_labels))
}
pub fn generation_params(prompt_format: PromptFormat) -> GenerationParams {
match prompt_format {
PromptFormat::SeedCoder1120 => SeedCoder1120Prompt::generation_params(),
_ => GenerationParams::default(),
}
}
pub fn write_codeblock<'a>(
path: &Path,
excerpts: impl IntoIterator<Item = &'a Excerpt>,
@@ -420,7 +432,7 @@ pub fn write_excerpts<'a>(
}
}
pub fn push_events(output: &mut String, events: &[predict_edits_v3::Event]) {
pub fn push_events(output: &mut String, events: &[Arc<predict_edits_v3::Event>]) {
if events.is_empty() {
return;
};
@@ -786,6 +798,7 @@ impl<'a> SyntaxBasedPrompt<'a> {
}
}
PromptFormat::MinimalQwen => unreachable!(),
PromptFormat::SeedCoder1120 => unreachable!(),
}
let push_full_snippet = |output: &mut String| {
@@ -896,19 +909,34 @@ fn declaration_size(declaration: &ReferencedDeclaration, style: DeclarationStyle
}
}
struct MinimalQwenPrompt {
events: Vec<Event>,
struct PromptData {
events: Vec<Arc<Event>>,
cursor_point: Point,
cursor_path: Arc<Path>, // TODO: make a common struct with cursor_point
included_files: Vec<IncludedFile>,
}
impl MinimalQwenPrompt {
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
#[derive(Default)]
pub struct GenerationParams {
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub stop: Option<Vec<String>>,
}
fn render(&self) -> String {
let edit_history = self.fmt_edit_history();
let context = self.fmt_context();
trait PromptFormatter {
fn render(&self, data: &PromptData) -> String;
fn generation_params() -> GenerationParams {
return GenerationParams::default();
}
}
struct MinimalQwenPrompt;
impl PromptFormatter for MinimalQwenPrompt {
fn render(&self, data: &PromptData) -> String {
let edit_history = self.fmt_edit_history(data);
let context = self.fmt_context(data);
format!(
"{instructions}\n\n{edit_history}\n\n{context}",
@@ -917,13 +945,17 @@ impl MinimalQwenPrompt {
context = context
)
}
}
fn fmt_edit_history(&self) -> String {
if self.events.is_empty() {
impl MinimalQwenPrompt {
const INSTRUCTIONS: &str = "You are a code completion assistant that analyzes edit history to identify and systematically complete incomplete refactorings or patterns across the entire codebase.\n";
fn fmt_edit_history(&self, data: &PromptData) -> String {
if data.events.is_empty() {
"(No edit history)\n\n".to_string()
} else {
let mut events_str = String::new();
push_events(&mut events_str, &self.events);
push_events(&mut events_str, &data.events);
format!(
"The following are the latest edits made by the user, from earlier to later.\n\n{}",
events_str
@@ -931,18 +963,18 @@ impl MinimalQwenPrompt {
}
}
fn fmt_context(&self) -> String {
fn fmt_context(&self, data: &PromptData) -> String {
let mut context = String::new();
let include_line_numbers = true;
for related_file in &self.included_files {
for related_file in &data.included_files {
writeln!(context, "<|file_sep|>{}", DiffPathFmt(&related_file.path)).unwrap();
if related_file.path == self.cursor_path {
if related_file.path == data.cursor_path {
write!(context, "<|fim_prefix|>").unwrap();
write_excerpts(
&related_file.excerpts,
&[(self.cursor_point, "<|fim_suffix|>")],
&[(data.cursor_point, "<|fim_suffix|>")],
related_file.max_row,
include_line_numbers,
&mut context,
@@ -961,3 +993,83 @@ impl MinimalQwenPrompt {
context
}
}
struct SeedCoder1120Prompt;
impl PromptFormatter for SeedCoder1120Prompt {
fn render(&self, data: &PromptData) -> String {
let edit_history = self.fmt_edit_history(data);
let context = self.fmt_context(data);
format!(
"# Edit History:\n{edit_history}\n\n{context}",
edit_history = edit_history,
context = context
)
}
fn generation_params() -> GenerationParams {
GenerationParams {
temperature: Some(0.2),
top_p: Some(0.9),
stop: Some(vec!["<[end_of_sentence]>".into()]),
}
}
}
impl SeedCoder1120Prompt {
fn fmt_edit_history(&self, data: &PromptData) -> String {
if data.events.is_empty() {
"(No edit history)\n\n".to_string()
} else {
let mut events_str = String::new();
push_events(&mut events_str, &data.events);
events_str
}
}
fn fmt_context(&self, data: &PromptData) -> String {
let mut context = String::new();
let include_line_numbers = true;
for related_file in &data.included_files {
writeln!(context, "# Path: {}\n", DiffPathFmt(&related_file.path)).unwrap();
if related_file.path == data.cursor_path {
let fim_prompt = self.fmt_fim(&related_file, data.cursor_point);
context.push_str(&fim_prompt);
} else {
write_excerpts(
&related_file.excerpts,
&[],
related_file.max_row,
include_line_numbers,
&mut context,
);
}
}
context
}
fn fmt_fim(&self, file: &IncludedFile, cursor_point: Point) -> String {
let mut buf = String::new();
const FIM_SUFFIX: &str = "<[fim-suffix]>";
const FIM_PREFIX: &str = "<[fim-prefix]>";
const FIM_MIDDLE: &str = "<[fim-middle]>";
write!(buf, "{}", FIM_PREFIX).unwrap();
write_excerpts(
&file.excerpts,
&[(cursor_point, FIM_SUFFIX)],
file.max_row,
true,
&mut buf,
);
// Swap prefix and suffix parts
let index = buf.find(FIM_SUFFIX).unwrap();
let prefix = &buf[..index];
let suffix = &buf[index..];
format!("{}{}{}", suffix, prefix, FIM_MIDDLE)
}
}

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