Compare commits

..

320 Commits

Author SHA1 Message Date
Kirill Bulatov
92db0259ab Create new tables for buffer data 2025-05-09 11:58:19 +03:00
tidely
851ab13f94 gpui: Bump blade, objc2, objc2-metal, and naga (#30347) 2025-05-09 11:40:12 +03:00
yeahyear
5249345312 chore: remove redundant word in comment (#30338)
remove redundant word in comment

Release Notes:

- remove redundant word in comment

Signed-off-by: yetyear <flite@outlook.com>
2025-05-09 07:33:20 +00:00
Antonio Scandurra
1b593f616f Include EditAgent's raw output when inspecting thread (#30337)
This allows us to debug the raw edits that were generated when people
report feedback, when running evals and when opening the thread as
Markdown.

Release Notes:

- Improved debug output for agent threads.
2025-05-09 06:58:45 +00:00
Maksim Bondarenkov
ea7756b362 deps: Update aws-lc-rs to 1.13.1 (#30332)
To pull in https://github.com/aws/aws-lc/pull/2381 which fixes build on
MinGW with Clang

Release Notes:

- N/A
2025-05-09 06:12:59 +00:00
Finn Evers
1ecd00a113 editor: Ensure minimap is shown when show_minimap is toggled to true (#30326)
Follow-up of #30285

This PR ensures the action added in the linked PR also works when the
user does not have the minimap enabled via settings. Currently, the
toggle only works when the user has already enabled the minimap in their
settings.

This happens because in


b4fbb9bc08/crates/editor/src/element.rs (L7160-L7164)

as well as


b4fbb9bc08/crates/editor/src/element.rs (L1542)

we check for the user configuration before reserving space for the
minimap as well as layouting it and because in

b4fbb9bc08/crates/editor/src/editor.rs (L16404)

with


b4fbb9bc08/crates/editor/src/editor_settings.rs (L132-L134)

we would not even create a minimap when the user disabled it via their
settings.

---

This PR fixes this by ensuring a minimap is created on the toggle issue
as well as lifting some of the restrictions. Since we are always only
returning a minimap in


b4fbb9bc08/crates/editor/src/editor.rs (L16443-L16445)

when `show_minimap` is set to `true`, we can assume in the rendering
code that if a minimap is present, it should be layouted and rendered no
matter if `ShowMinimap` is currently set to `Never`. We can do this
since `show_minimap` always reflects the current user configuration, see


b4fbb9bc08/crates/editor/src/editor.rs (L18163-L18164)

I also removed the minimap deletion/recreation on the toggling of
`show_minimap`, since this is not really needed - once we have stored a
minimap editor within the editor, `show_minimap` is sufficient to ensure
that it is only shown when the user requests it. Notice that we still
will never create a minimap unless neccesary.

Lastly, I updated the `supports_minimap` check to account for the fact
that the minimap is currently disabled entirely for multibuffers.

--- 

One thing I ~~did not tackle here~~ tackled in the second commit is that
due to `show_minimap` now being exposed to the user, it is possible to
enable the minimap for all full mode editors, e.g. the agent text thread
editor

<img width="592" alt="grafik"
src="https://github.com/user-attachments/assets/5f6c0e8b-45f9-44e8-9625-9d51c1480f98"
/>

which should most likely not be possible when the minimap is
programmatically disabled.

Release Notes:

- N/A
2025-05-09 08:32:31 +03:00
Max Brunsfeld
29c31f020e Implement rendering of images with data urls in markdown (#30322)
Fixes #28266

![Screenshot 2025-05-08 at 5 08
21 PM](https://github.com/user-attachments/assets/774d2dde-3f2d-466c-8eb1-c67badbd89e4)

Release Notes:

- Added support for rendering images with data URLs in markdown. This
can show up in hover documentation provided by language servers.

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-05-08 18:26:24 -07:00
Agus Zubiaga
c512d43e8c agent: Render edit tool error as markdown (#30325)
Release Notes:

- agent: Render edit tool error as markdown and allow selecting it
2025-05-09 01:18:52 +00:00
Smit Barmase
05a6c31ad8 languages: Fix python indent block for more keywords (#30323)
Add `with`, `while`, `match`, `class` and `case` keywords as indent
block.

Release Notes:

- N/A
2025-05-09 05:51:33 +05:30
Agus Zubiaga
9810745465 agent: Fix autoscrolling to history entry (#30321)
We were still using entry indexes to scroll, but the list now includes
the separators as items, so the indexes need to be translated

Release Notes:

- agent: Fix autoscrolling to history entry when navigating via keyboard
2025-05-08 20:48:57 -03:00
Agus Zubiaga
4b61d4ba6a agent: Fix message editor's button positions when expanded (#30311)
Fixes an issue introduced by #29959 which caused the message editor to
overflow from the agent panel bounds, making the bottom buttons
invisible when the editor was expanded (cmd+esc).

Fixing this required changing the base structure of the agent panel, but
things seem to work as expected:


https://github.com/user-attachments/assets/fc4c97fb-f7cb-4f54-a268-c30fbcb1649f


Release Notes:

- agent: Fix message editor's button positions when expanded
2025-05-08 20:36:19 -03:00
Kirill Bulatov
b4fbb9bc08 Use ESC to cancel dragging in Zed (#30318)
Closes https://github.com/zed-industries/zed/issues/11887

ESC is always captured in terminal due to 


980bfae331/crates/terminal/src/terminal.rs (L1339-L1353)

so this part is not fixed.

Otherwise, all other drags are cancelled when ESC is pressed:


https://github.com/user-attachments/assets/6e70a1e5-c244-420b-9dec-ae2ac2997a59


Release Notes:

- Allowed to use ESC to cancel dragging in Zed
2025-05-08 22:58:38 +00:00
Cole Miller
83378b856f agent: Show checkmark for current profile, not default profile (#30314)
Closes #ISSUE

Release Notes:

- agent: Fixed a bug that caused the profile selector to display a
checkmark next to the wrong profile.
2025-05-08 18:17:55 -04:00
Shardul Vaidya
648d0054de bedrock: Fix UX bug (#28350)
Closes #29072, #28390, 

Release Notes:

- AWS Bedrock: Fixed case where user couldn't delete manually added AWS
credentials.

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-08 22:09:18 +00:00
Kirill Bulatov
a8312d623d Allow to temporarily toggle diagnostics in the editor (#30316)
* Adds a `diagnostics_max_severity: null` editor settings that has
previous hardcoded default, `warning`
* Make inline diagnostics to inherit this setting by default (can be
overridden with its own max_severity setting)
* Allows to toggle diagnostics in the editor menu and via new action,
`editor::ToggleDiagnostics`

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

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-08 21:47:32 +00:00
Smit Barmase
9e5d115e72 editor: Fix TypeScript auto-import breaking generic function calls (#30312)
Closes #29982

When auto-importing TypeScript functions with generic type arguments
(like `useRef<HTMLDivElement>(null)`), the language server returns
snippets with placeholders (e.g., `useRef(${1:initialValue})$0`). While
useful for new function calls, this behavior breaks existing code when
renaming functions that already have parameters.

For example, completing `useR^<HTMLDivElement>(null)` incorrectly
results in `useRef(initialValue)^<HTMLDivElement>(null)`.

Related upstream issue:
https://github.com/microsoft/TypeScript/issues/51758
Similar workaround fix:
https://github.com/pmizio/typescript-tools.nvim/pull/147

Release Notes:

- Fixed TypeScript auto-import behavior where functions with generic
type arguments (like `useRef<HTMLDivElement>(null)`) would incorrectly
insert snippet placeholders, breaking the syntax.
2025-05-09 03:13:22 +05:30
Marshall Bowers
822580cb12 collab: Check if the user has a payment method before upgrading to Zed Pro (#30310)
This PR adds a check for if the user has a payment method before
attempting to upgrade them to Zed Pro.

Release Notes:

- N/A
2025-05-08 20:58:36 +00:00
Cole Miller
8b764a5477 Add a test for remote tool use by the agent (#30289)
- Adds a new smoke test for the use of the read_file tool by the agent
in an SSH project
- Fixes the SSH shutdown sequence to use a timer from the app's executor
instead of always using a real timer
- Changes the main executor loop for tests to advance the clock
automatically instead of panicking with `parked with nothing left to
run` when there is a delayed task

Release Notes:

- N/A
2025-05-08 16:53:04 -04:00
Marshall Bowers
660b4cee76 collab: Add intent for updating payment method (#30306)
This PR adds a new `ManageSubscriptionIntent` for initiating a session
to update the user's payment method.

Release Notes:

- N/A
2025-05-08 20:17:19 +00:00
Peter Tripp
e74ae89c84 Add support for ctrl-backspace in terminal (delete word backward) (#30139) 2025-05-08 20:00:28 +00:00
gak
b0a6146362 docs: Add missing quote in JSON snippet (#30303)
Release Notes:

- N/A
2025-05-08 19:25:05 +00:00
Richard Feldman
77945fc905 Support find_project_path being given absolute paths (#30283)
Sometimes models return absolute paths even though we ask them not to
(including sometimes returning `/dev/null`). Currently we assume we're
always given a relative path, which leads to a panic in debug builds.
Now we just support being given absolute paths.

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-08 15:04:51 -04:00
Marshall Bowers
6827bf114a collab: Remove legacy claims from LLM token (#30294)
This PR removes some legacy claims related to the old billing from the
LLM token.

We already stopped reading this in the LLM Worker.

Also removed an outdated feature flag check that restricted access to
obtaining an LLM token.

Release Notes:

- N/A
2025-05-08 14:32:54 -04:00
Kirill Bulatov
2b6280ad56 Add minimap into the editor controls (#30285)
Follow-up of https://github.com/zed-industries/zed/pull/26893

Release Notes:

- N/A
2025-05-08 17:58:24 +00:00
Marshall Bowers
f21780cef3 Remove individual URL overrides for LLM service (#30290)
This PR removes the individual URL overrides for the LLM service.

We initially had `ZED_PREDICT_EDITS_URL` to allow for directing traffic
to the LLM Worker back when there was still the split of the
Collab-based LLM Service and the Cloudflare-based LLM Worker.

But now that all of the LLM functionality has been moved into the
Worker, we can just direct all traffic there.

Release Notes:

- N/A
2025-05-08 17:54:46 +00:00
Kirill Bulatov
c64dc82e21 Add a terminal::RerunTask action (#30288)
Bounded this action to the same defaults `task::Rerun` is bound to.

Unlike the `task::Rerun` which will always rerun the latest task, this
command reruns the current task tab, if focused.
The task is not in scope when the terminal pane is not focused, and
falls back to the regular rerun if invoked on a task-less terminal tab.

This way, we can add a proper tooltip to the terminal tab reruns:

<img width="231" alt="image"
src="https://github.com/user-attachments/assets/2cdd7458-5ba2-4cc7-a10b-3e2db059f1ca"
/>


Release Notes:

- Added `terminal::RerunTask` task action
2025-05-08 17:39:52 +00:00
Marshall Bowers
9268308543 assistant_context_editor: Remove suggest edits (#30286)
This PR removes the code for the "Suggest Edits" functionality from
Assistant1.

This feature was already disabled entirely with the launch of the Agent,
we're just cleaning up the unused code.

Release Notes:

- N/A
2025-05-08 17:27:49 +00:00
Danilo Leal
7fb52ddf32 Add a divider below "Usage" in the Edit Prediction menu (#30284)
As it felt untidy without it.

Release Notes:

- N/A
2025-05-08 14:19:51 -03:00
Anthony Eid
dc01aef0cf debugger: Update New Session Modal (#30018)
This PR simplifies the new session modal by flattening its three modes
and updating the UI to be less noisy. The new UI also defaults to the
Debug Scenario Picker, and allows users to save debug scenarios created
in the UI to the active worktree's .zed/debug.json file.


Release Notes:

- N/A
2025-05-08 16:19:14 +00:00
Michael Sloan
e9a756b5fc Make copilot::SignIn open sign-in modal when needed (#30239)
Also:

* Makes sign out show status notifications and errors.
* Reinstall now prompts for sign-in after start.

Addresses some of #29250, but not all of it.

Release Notes:

- N/A
2025-05-08 14:52:07 +00:00
Kirill Bulatov
203cb7a7a2 Restyle notification close control (#30262)
Follow-up of https://github.com/zed-industries/zed/pull/30015

Merges suppress and close buttons into one, with `shift` changing the
state and showing different tooltips.
Currently, there's no tooltip for notification suppress action, hence
none is displayed in the video:


https://github.com/user-attachments/assets/678c4d76-a86e-4fe9-8d7b-92996470a8a8

Release Notes:

- N/A
2025-05-08 14:10:30 +00:00
Kirill Bulatov
93b88a905a Remove not implemented minimap settings (#30253)
Closes https://github.com/zed-industries/zed/issues/30250

Based on
https://github.com/zed-industries/zed/pull/26893#issuecomment-2847338831

Release Notes:

- N/A
2025-05-08 13:57:41 +00:00
Kirill Bulatov
85fda90993 Do not remove the item from pane twice (#30254)
Probably a merge artifact?

Release Notes:

- N/A
2025-05-08 13:30:20 +00:00
Marshall Bowers
b343a8aa22 language_models: Improve subscription states in the Agent configuration view (#30252)
This PR improves the subscription states in the Agent configuration view
to the new billing system.

Zed Free (legacy):

<img width="638" alt="Screenshot 2025-05-08 at 8 42 59 AM"
src="https://github.com/user-attachments/assets/7b62d4c1-2a9c-4c6a-aa8f-060730b6d7b3"
/>

Zed Free (new):

<img width="640" alt="Screenshot 2025-05-08 at 8 43 56 AM"
src="https://github.com/user-attachments/assets/8a48448e-813e-4633-955d-623d3e6d603c"
/>

Zed Pro trial:

<img width="641" alt="Screenshot 2025-05-08 at 8 45 52 AM"
src="https://github.com/user-attachments/assets/1ec7ee62-e954-48e7-8447-4584527307c9"
/>

Zed Pro:

<img width="636" alt="Screenshot 2025-05-08 at 8 47 21 AM"
src="https://github.com/user-attachments/assets/f934b2e3-0943-4b78-b8dc-0a31e731d8fb"
/>

Release Notes:

- agent: Improved the subscription-related information in the
configuration view.
2025-05-08 09:10:50 -04:00
Ben Brandt
3a3d3c05e8 Improve token counting for OpenAI models (#30242)
tiktoken_rs is a bit behind (and even upstream tiktoken doesn't have all
of these models)

We were incorrectly using the cl100k tokenizer for some models that
actually use the o200k tokenizers. So that is updated.

I also made the match arms specific so that we do a better job of
catching whether or not tiktoken-rs accurately supports new models we
add in.

I will also do a PR upstream to see if we can move some of this logic
back out if tiktoken better supports the newer models.

Release Notes:

- Improved tokenizer support for openai models.
2025-05-08 13:09:29 +00:00
Piotr Osiewicz
ee56706d15 debugger: Fix up Rust test tasks definitions (#30232)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-05-08 14:39:56 +02:00
Oleksiy Syvokon
3cc8850a58 settings: Migration for fixing duplicated agent keys (#30237)
As a byproduct, this fixes bug where it's impossible to change Agent
profile

Closes #30000 

Release Notes:

- N/A
2025-05-08 12:38:19 +00:00
Antonio Scandurra
9f6809a28d Reuse conversation cache when streaming edits (#30245)
Release Notes:

- Improved latency when the agent applies edits.
2025-05-08 14:36:34 +02:00
Bennet Bo Fenner
032022e37b agent: Tweak wording when configuring profiles (#30027)
cc @danilo-leal 

Release Notes:

- N/A

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-08 09:11:24 -03:00
Piotr Osiewicz
b091581e4b debugger/extensions: Revert changes to extension store related to language config (#30225)
Revert #29945 

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
2025-05-08 14:01:39 +02:00
张小白
20387f24aa windows: Fix atomic write (#30234)
Superseded #30222

On Windows, `MoveFileExW` fails if another process is holding a handle
to the file. This PR fixes that issue by switching to `ReplaceFileW`
instead.

I’ve also added corresponding tests.

According to [this Microsoft research
paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2006/04/tr-2006-45.pdf)
and the [official
documentation](https://learn.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf#applications-updating-a-single-file-with-document-like-data),
`ReplaceFileW` is considered an atomic operation. even though the
official docs don’t explicitly state whether `MoveFileExW` or
`ReplaceFileW` is guaranteed to be atomic.

Release Notes:

- N/A
2025-05-08 19:57:16 +08:00
tidely
4b5158b168 indexed_docs: Remove some unnecessary cloning (#30236)
Other small patch to reduce allocations.

`.iter().cloned().collect()` calls `Clone` per element, whereas
`.into_iter().collect()` preallocates the `Vec`.

The Zed repo for example has up to 1700 packages on some build
configurations, meaning this change theoretically saves up to 1699
allocations. It's likely the compiler has already optimized this away,
but it's good to be explicit.

Release Notes:

- N/A
2025-05-08 10:59:56 +00:00
tidely
a61958e886 language: Remove some unnecessary cloning (#30229)
Another tiny patch to reduce allocations

`.iter().cloned().collect()` calls `Clone` per element, whereas
`into_iter().collect()` preallocates memory

Release Notes:

- N/A
2025-05-08 12:57:19 +02:00
Ben Brandt
d06d0e6a94 Use fit instead of center for Agent following (#30228)
Makes it easier to review the Agent edits since more of the previous
edits will be visible on screen.

Release Notes:

- N/A
2025-05-08 10:50:17 +00:00
tidely
e8b67872ed workspace: Remove excess clone (#30226)
Remove excess clone when invoking callback in workspace

Release Notes:

- N/A
2025-05-08 12:31:43 +02:00
Smit Barmase
fcf066aff5 fs: Fall back from atomic write to regular fs write when file handle is in use on Windows (#30222)
Closes #30054

For reference, another way to work around this is to drop the file
handle which we can't do in this case, as it would require reopening the
settings.json worktree, which is a rather unpleasant fix.

Another approach might be to open the file handle with some special
flags, but I couldn't get that to work at the time of writing.

Release Notes:

- Fixed "Backup and Update" in settings migration not working on
Windows.
2025-05-08 15:42:32 +05:30
Ben Brandt
b4109a2376 Prevent keybindings from triggering requests that should be disabled (#30221)
Extracts authorization logic to a single method and add early
returns in message handlers to prevent sending requests when the model
configuration is invalid or terms haven't been accepted.

This was allowing for the TOS popup to show up even for logged out users
because they could bypass the disabled button with the keybinding.

Now the behavior should be the same either way, that the request isn't
made unless they can send it.

The text thread already has a banner to tell the user to configure a
model provider, so I don't think we need to pop up a separate modal,
since the button is disabled anyway.

Release Notes:

- N/A
2025-05-08 10:03:58 +00:00
Bennet Bo Fenner
6565c091e4 agent: Improve Gemini tool schema compatibility (#30216)
Closes #30056

Apparently the API supports the "default" field now, so we can remove
that transformation.
However, optional is not supported

See https://ai.google.dev/api/caching#Schema

Release Notes:

- agent: Improve tool schema compatibility for Gemini models
2025-05-08 09:06:35 +00:00
Conrad Irwin
d39c220f26 Add rdbg for Ruby (#30126)
Release Notes:

- N/A
2025-05-08 09:37:20 +01:00
Finn Evers
1ec466b728 editor: Ensure scrollbar thumb is not layouted when content size is smaller than viewport (#30189)
As discussed and explained in
https://github.com/zed-industries/zed/pull/26893#discussion_r2074102719

This PR fixes an issue where we would have zero-divisions during
scrollbar layouting for small files.

This happened due to the fact that for small files, 


9c1b2afa49/crates/editor/src/element.rs (L8562-L8563)

would be `NaN`, since `(total_text_units - text_units_per_page).max(0.)`
would return `0.`, which we would divide by.

However, this was neccessary to be in place, as this prevented the
scroll thumb from being rendered for small files: Due to this being
`NaN`, the thumb origin would be `Pixels(NaN)`, which prevented the
rendering of the scrollbar thumb.

This PR fixes this behavior by accounting for this scenario and changing
the thumb bounds to be an `Option<Bounds<Pixels>>` instead. This
furthermore has the advantage that we have to compute the thumb only
once and storing it in the layout, which was previously not possible.

Most notably, this enables scrollbar markers to show for smaller files:


https://github.com/user-attachments/assets/9fa5d240-8795-4fae-9933-aed144df4f5e

Currently, no markers are shown due to the fact that `Pixels(NaN)` is
set as the origin point.

Also, I changed that the cursor style will only be changed on the
scrollbar hitbox when we will actually show a thumb. This way, for small
files (where viewport > content size) the cursor will not change when a
user hovers with their mouse over the scrollbars hitbox.

Theoretically, I could also include the change mentioned in
https://github.com/zed-industries/zed/pull/26893#discussion_r2076316956
here. Given the introduction of the minimap as well as #29316 and the
cursor style taken care of here, removing the guard would not change
anything and creates the possibility to soon introduce scrollbars for
auto height editors. Please let me know whether we want to have this in
this PR or whether I shall create a seperate one.

Release Notes:

- Enabled scrollbar marker rendering for small files.
2025-05-08 07:06:10 +00:00
Finn Evers
a127ff4a4f search: Do not consider filters if they are toggled off (#30162)
Closes #30134

This PR ensures that path filters are only applied to searches when the
filters are actually enabled (and visible).

Release Notes:

- Fixed the project search considering included and excluded filters
after toggling them off.
2025-05-08 09:42:40 +03:00
Piotr Osiewicz
f16f4303f4 debugger: Fix spawn straight away behavior when there's a single non-debug task on the line (#30154)
Closes #ISSUE

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2025-05-08 08:42:32 +02:00
Ben Brandt
3615d6d96c Load Profile state from Thread and tie visibility to the thread's model (#30090)
When deciding if a model supports tools or not, we weren't reading from
the configured model in a given thread.

This also stores the profile on the thread, which matches the behavior
of the Model and Max Mode, which we also already store per thread.

Hopefully this helps alleviate some confusion.

Release Notes:

- agent: Save profile selection per-Agent thread
2025-05-07 22:36:08 -04:00
versecafe
02ed4aefb8 mistral: Add new Mistral medium model (#30171)
Release Notes:

- Added `mistral-medium` to the Mistral provider.
2025-05-07 21:57:15 -04:00
Marshall Bowers
6cc6e4d4b3 agent: Rename a number of constructs from Assistant to Agent (#30196)
This PR renames a number of constructs in the `agent` crate from the
"Assistant" terminology to "Agent".

Not comprehensive, but it's a start.

Release Notes:

- N/A
2025-05-08 01:18:51 +00:00
Julia Ryan
d6c7cdd60f Add :h[elp] vim command (#30179)
@jyn514 mentioned that this would be nice to have while trying out zed,
and it seemed simple enough so I added it.

Release Notes:

- Added `OpenDocs` action to open Zed's docs in a browser, aliased to
`:h[elp]` in vim.
2025-05-07 17:26:42 -07:00
Max Brunsfeld
37010aac6b Allow opening the FS root dir as a remote project (#30190)
### Todo

* [x] Allow opening `ssh://username@host:/` from the CLI
* [x] Allow selecting `/` in the `open path` picker
* [x] Allow selecting the home directory in the `open path` picker

Release Notes:

- Changed the initial state of the SSH project picker to show the full
path to your home directory on the remote machine, instead of `~`.
- Added the ability to open `/` as a project folder over SSH

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-07 16:50:57 -07:00
Marshall Bowers
6ac2f4e6a5 Remove assistant crate (#30168)
This PR removes the `assistant` crate, as it is no longer used.

Release Notes:

- N/A
2025-05-07 23:05:38 +00:00
Kirill Bulatov
011aa715cf Fix workspace update notifications not being suppressed (#30180)
Follow-up of https://github.com/zed-industries/zed/pull/30015

Release Notes:

- N/A
2025-05-07 20:39:31 +00:00
Vitaly Slobodin
3339c84cdd ruby: Update documentation about new LS activation sequence (#30160)
Hi, this pull request updates the Ruby extension documentation to
reflect new language server activation sequence and autoinstallation
shipped in
[v0.7.0](https://github.com/zed-extensions/ruby/releases/tag/v0.7.0).

Release Notes:

- N/A
2025-05-07 23:35:48 +03:00
Finn Evers
9c1b2afa49 theme: Add scrollbar_thumb_active_background color (#30177)
Follow-up to #28064

This PR adds the `scrollbar_thumb_active_background` to themes and uses
it for the editor scrollbars to color these whilst they are being
dragged. This way, we provide the best customizabiliy for the scrollbars
and enable theme authors to add good contrasts between all the three
states `ScrollbarThumbState::Idle`, `ScrollbarThumbState::Hovered` and
ScrollbarThumbState::Dragging`.

It also adds this to the VsCode theme importer so any future imported
themes will have this set as well.

Whenever the property is not set, I decided it is best to fall back to
the normal `thumb_background` for the time being, as this way the
distinction and contrast between hovered and active state is better than
having the same color for hovering and dragging the scrollbar.

Example with active color set via `experimental.theme_overrides` in the
settings:


https://github.com/user-attachments/assets/9934e75b-6e0a-4a41-90ba-bfffb89865e7

Release Notes:

- Added the `scrollbar.thumb.active_background` color to themes. Theme
authors can use this property in combination with
`scrollbar.thumb.hover_background` to customize the color of the editor
scrollbar thumbs while these are hovered or being dragged.
2025-05-07 23:15:32 +03:00
Evan Simkowitz
607a9445fc editor: Add minimap (#26893)
## Overview

This PR adds the minimap feature to the Zed editor, closely following
the [design from Visual Studio
Code](https://code.visualstudio.com/docs/getstarted/userinterface#_minimap).
When configured, a second instance of the editor will appear to the left
of the scrollbar. This instance is not interactive and it has a slimmed
down set of annotations, but it is otherwise just a zoomed-out version
of the main editor instance. A thumb shows the line boundaries of the
main viewport, as well as the progress through the document. Clicking on
a section of code in the minimap will jump the editor to that code.
Dragging the thumb will act like the scrollbar, moving sequentially
through the document.

![screenshot of Zed with three editors open and the minimap enabled,
showing the
slider](https://github.com/user-attachments/assets/4178d23a-a5ea-4e38-b871-06dd2a8f9560)

## New settings

This adds a `minimap` section to the editor settings with the following
keys:

### `show`

When to show the minimap in the editor.
This setting can take three values:
1. Show the minimap if the editor's scrollbar is visible: `"auto"`
2. Always show the minimap: `"always"`
3. Never show the minimap: `"never"` (default)

### `thumb`

When to show the minimap thumb.
This setting can take two values:
1. Show the minimap thumb if the mouse is over the minimap: `"hover"`
2. Always show the minimap thumb: `"always"` (default)

### `width`

The width of the minimap in pixels.

Default: `100`

### `font_size`

The font size of the minimap in pixels.

Default: `2`

## Providing feedback

In order to keep the PR focused on development updates, please use the
discussion thread for feature suggestions and usability feedback: #26894


## Features left to add

- [x] fix scrolling performance
- [x] user settings for enable/disable, width, text size, etc.
- [x] show overview of visible lines in minimap
- [x] clicking on minimap should navigate to the corresponding section
of code
- ~[ ] more prominent highlighting in the minimap editor~
- ~[ ] override scrollbar auto setting to always when minimap is set to
always show~

Release Notes:

- Added minimap for high-level overview and quick navigation of editor
contents.

---------

Co-authored-by: MrSubidubi <dev@bahn.sh>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-07 23:11:09 +03:00
Finn Evers
902931fdfc git_ui: Only register conflict addon for full mode editors (#30049)
Noticed this whilst working on #26893 

This PR prevents that single line and auto height editors have a
conflict addon attached (and are observed for any excerpt changes).


From how I understand it, it does not really make sense to register the
conflict addon for single line or auto height editors.

These editors will never show a conflict nor will they be used to
resolve one. Furthermore, neither of these ever have a project attached
upon creation:


00c5f57575/crates/editor/src/editor.rs (L1385)


00c5f57575/crates/editor/src/editor.rs (L1403)


00c5f57575/crates/editor/src/editor.rs (L1415)

so their buffers will never be added here:


00c5f57575/crates/git_ui/src/conflict_view.rs (L116-L120)

Thus, we could potentially even extend the check with an additional `||
editor.project.is_none()`. Yet, as I am not entirely sure how all of
this exactly works, I left this out for now, but I can definitely add
this if wanted.

Release Notes:

- N/A
2025-05-07 16:10:08 -04:00
Max Brunsfeld
3c128ef83f Avoid empty schema in copilot dummy tool (#30178)
Copilot chat still returns a 400 if the dummy tool uses the `{}` schema.

This is a follow-up to https://github.com/zed-industries/zed/pull/30007.

Release Notes:

- Fixed a bug where agent edits would fail when using GitHub Copilot
Chat.

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-07 20:08:38 +00:00
Smit Barmase
0d726603ce editor: Fix punctuation in JSX tags breaks the linked edit to the closing tag (#30167)
Closes #29983

While we only care about `.`, just enabling punctuation in case of
linked edits shouldn't hurt.

Release Notes:

- Fixed JSX component names with periods (e.g., <Animated.View>) now
maintain linked edits between opening and closing tags.
2025-05-08 00:47:00 +05:30
Finn Evers
466a53b51e title_bar: Add link indicator to current plan entry in user menu (#30153)
This PR adds a link indicator to the `Current Plan: ...` entry in the
user menu

<img width="232" alt="link_indicator"
src="https://github.com/user-attachments/assets/89c6247c-08cb-4cac-b136-5c5b71f1a975"
/>

to indicate this opens an external link and not something within Zed.

Release Notes:

- N/A
2025-05-07 14:25:38 -04:00
Finn Evers
358c324e26 editor: Use default gutter margin instead of horizontal_padding for horizontal content padding (#30138)
This PR changes the way a horizontal margin is added in editors. It
removes the possibility to set a custom `horizontal_padding` for an
editor and utilizes the default `gutter_dimension` instead.

This change is made to ensure that no issues with soft-wrapping occurs
for any editor that has a `horizontal_margin` set (see #26893 for more
context on the implications here`. Furthermore, it ensures that the text
actually renders properly when scrolling horizontally and is not
cut-off.

### Horizontal padding:

| `main` | This PR |
| --- | --- |
| ![main
padding](https://github.com/user-attachments/assets/4e7ea020-f92d-4f28-8cc1-89d0b0350683)
| ![PR
padding](https://github.com/user-attachments/assets/a05bae17-c384-431b-bb79-a1fffe7a29d7)
|

### Editor horizontally scrolled:

| `main` | This PR |
| --- | --- |
| ![main
scrolled](https://github.com/user-attachments/assets/1a30156f-6c08-4cf9-94aa-9d087c0408cc)
| ![pr
scrolled](https://github.com/user-attachments/assets/d0daa72e-3b02-479b-aea0-41e1a376c567)
|

Notice the difference at the horizontal borders.

The margin added for the `edit_file_tool` was 4 pixels. The `descent`,
whilst not exactly, is roughly the same here and also scales with the
font size nicely. Furthermore, it seems that the
`gutter_dimensions.margin` should be present anyway, given the following
comment


0b00256f58/crates/editor/src/element.rs (L6887-L6889)

so ensuring this property is actually set and not 0 seems to be
reasonable given the circumstances.

Please note though that this will apply to all editors in the app.
Again, this seems like it should be the case anyway, just wanted to
mention this again.

Should the fix like this not be wanted, I can change this here so that
the `horizontal_margin` is better accounted for when soft-wrapping in an
editor. Feel free to let me know in this case.

Release Notes:

- N/A
2025-05-07 18:23:54 +00:00
Kirill Bulatov
6e19c9b141 Add a way to clear activity indicator (#30156)
Follow-up of https://github.com/zed-industries/zed/pull/30015

* Restyles the dismiss and close buttons a bit: change the dismiss icon
and add tooltips with the bindings to both
* Allows ESC to clear any status that's in the activity indicator now,
if all notifications are cleared: this won't suppress any further status
additions though, so statuses may resurface later

Release Notes:

- Added a way to clear activity indicator
2025-05-07 17:50:52 +00:00
Agus Zubiaga
77ac82587a agent: Improve consecutive tool use callout spacing (#30145)
Release Notes:

- agent: Fix "consecutive tool use limit" callout spacing
2025-05-07 17:43:04 +00:00
Smit Barmase
7c76cee16d language: Fix indent suggestions for significant indented languages like Python (#29625)
Closes #26157

This fixes multiple cases where Python indentation breaks:
- [x] Adding a new line after `if`, `try`, etc. correctly indents in
that scope
- [x] Multi-cursor tabs correctly preserve relative indents
- [x] Adding a new line after `else`, `finally`, etc. correctly outdents
them
- [x] Existing Tests

Future Todo: I need to add new tests for all the above cases.

Before/After:

1. Multi-cursor tabs correctly preserve relative indents


https://github.com/user-attachments/assets/08a46ddf-5371-4e26-ae7d-f8aa0b31c4a2

2. Adding a new line after `if`, `try`, etc. correctly indents in that
scope


https://github.com/user-attachments/assets/9affae97-1a50-43c9-9e9f-c1ea3a747813

Release Notes:

- Fixes indentation-related issues involving tab, newline, etc for
Python.
2025-05-07 23:05:42 +05:30
Finn Evers
22ad207baf agent: Fix profile menu hover flicker after settings update (#30109)
Closes #30091
Follow-up to #29958

This PR fixes the profile menu flickering due to the documentation aside
after updating the agent dock position over the settings file.

The problem arose because the `documentation_side` could get out of sync
with the actual agent panel dock position. The `documentation_side` was
only updated whenever the user changed the agent panel position using
the UI, but not when updating the position in the settings file.

You can reproduce this easily by changing the `agent.dock` position to
the opposite site in your settings, which will make the profile menu
flicker again in some scenarios due to the de-sync.

This PR fixes this behavior by computing the position during render,
thus the actual set panel position and the documentation position can
never get out of sync

Release Notes:

- Fixed the agent profile menu flickering after updating the assistant
panel dock position in the settings.
2025-05-07 10:17:59 -07:00
Marshall Bowers
4469b7339f language_models: Update copy for Zed Pro subscription (#30152)
This PR updates the copy around the Zed Pro description to be more
accurate.

Release Notes:

- agent: Updated some copy about Zed Pro in the configuration view.
2025-05-07 17:15:02 +00:00
Peter Tripp
ea769455e4 Legal Terms: May 6th 2025 update (#30151)
Updated terms for Agent panel launch.

Release Notes:

- N/A
2025-05-07 17:14:02 +00:00
Antonio Scandurra
89430a019c Fix agent reading and editing files over SSH (#30144)
Release Notes:

- Fixed a bug that would prevent the agent from working over SSH.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
2025-05-07 17:07:01 +00:00
Marshall Bowers
582ad845b9 collab: Return trial_started_at alongside billing preferences (#30143)
This PR updates the `GET /billing/preferences` endpoint to return the
user's `trial_started_at` timestamp alongside the billing preferences.

Release Notes:

- N/A
2025-05-07 16:49:54 +00:00
Conrad Irwin
1b3140d4ab Delete code actions indicator (#30140)
This conflicts for space with breakpoints, and seems borderline in terms
of utility.

We could consider bringing it back in a way that is closer to the
cursor, or be content with our right-click menu discovery.

Release Notes:

- Remove the code actions indicator from the gutter. It is still
available from the right click menu, or with the keyboard shortcut.
2025-05-07 17:33:42 +01:00
Danilo Leal
625e45bac0 agent: Improve onboarding modal background illustration (#30137)
Tone down the grid background a bit more so text is more legible.

Release Notes:

- N/A
2025-05-07 13:24:36 -03:00
Marshall Bowers
d50562ed81 collab: Remove code for syncing token-based billing events (#30130)
This PR removes the code related to syncing token-based billing events
to Stripe.

We don't need this anymore with the new billing.

Release Notes:

- N/A
2025-05-07 16:11:51 +00:00
Marshall Bowers
a34fb6f6b1 Send up Zed version with edit prediction and completion requests (#30136)
This PR makes it so we send up an `x-zed-version` header with the
client's version when making a request to llm.zed.dev for edit
predictions and completions.

Release Notes:

- N/A
2025-05-07 15:44:30 +00:00
Danilo Leal
5ca114be24 agent: Make feedback buttons more minimal (#30133)
Also swapped out the svgs for `ThumbsDown` and `ThumbsUp`, and added
`DocumentText`.

Release Notes:

- N/A
2025-05-07 12:43:46 -03:00
Richard Feldman
fcb9706022 Improve Ollama tool use (#30120)
<img width="458" alt="Screenshot 2025-05-07 at 9 37 39 AM"
src="https://github.com/user-attachments/assets/80f8a9b8-6a13-4e84-b91d-140e11475638"
/>

<img width="603" alt="Screenshot 2025-05-07 at 9 37 33 AM"
src="https://github.com/user-attachments/assets/7fe67a68-3885-4a0e-a282-aad37e92068b"
/>


Release Notes:

- Ollama models no longer require the supports_tools field in settings
(defaults to false)

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-07 15:37:06 +00:00
Peter Tripp
0a44048af8 Lowercase settings.json for vscode settings importer (#30131)
Closes: https://github.com/zed-industries/zed/issues/30117

Release Notes:

- N/A
2025-05-07 15:13:50 +00:00
Kirill Bulatov
a4aa446a20 Better match path-like strings in terminal (#30087)
Start to capture `foo/bar:20:in`-like strings as valid pointers to line
20 in a file

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

Release Notes:

- Fixed terminal cmd-click not registering `foo/bar:20:in`-like paths
2025-05-07 14:47:23 +00:00
Anthony Eid
a4e26e0710 debugger: Reduce indent level and step size in variable list (#30122)
This improves the look of the variable list when it's the debug panel is
docked on a side

### Before

![image](https://github.com/user-attachments/assets/6e886fc4-1579-4f4c-bda5-6850d080f34b)
### After

![image](https://github.com/user-attachments/assets/5c4528e5-aa01-4035-9f67-fdf22ce82e34)

Release Notes:

- N/A
2025-05-07 14:06:14 +00:00
Danilo Leal
542c4a3d35 docs: Add section about agent notification (#30121)
Release Notes:

- N/A
2025-05-07 10:41:49 -03:00
Danilo Leal
e44d167e56 docs: Add section about following the agent (#30119)
Release Notes:

- N/A
2025-05-07 10:38:15 -03:00
James Roberts
c1d4f0873d docs: Fix links in ai section (#30116)
There were a number of broken links in the new agent panel docs. This
fixes them by replacing `(/ai/` with `(./`

Release Notes:

- N/A
2025-05-07 10:22:41 -03:00
James Roberts
48dfdc416b docs: Fix links in ai section (#30116)
There were a number of broken links in the new agent panel docs. This
fixes them by replacing `(/ai/` with `(./`

Release Notes:

- N/A
2025-05-07 10:20:40 -03:00
Andrey Sitnik
2618191785 Add TypeScript support to ESLint flat config (#30044)
Sync ESLint flat config names with [the latest
docs](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file).
New ESLint has native support for `eslint.config.ts`

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-07 12:49:00 +00:00
Anthony Eid
1a520990cc debugger: Add inline value tests (#29815)
## Context

This PR improves the accuracy of our inline values for Rust/Python. It
does this by only adding inline value hints to the last valid use of a
variable and checking whether variables are valid within a given scope
or not.

We also added tests for Rust/Python inline values and inline values
refreshing when stepping in a debug session.

### Future tasks
1. Handle functions that have inner functions defined within them.
2. Add inline values to variables that were used in inner scopes but not
defined in them.
3. Move the inline value provider trait and impls to the language trait
(or somewhere else).
4. Use Semantic tokens as the first inline value provider and fall back
to tree sitter
5. add let some variable statement, for loops, and function inline value
hints to Rust.
6. Make writing tests more streamlined. 
6.1 We should be able to write a test by only passing in variables,
language, source file, expected result, and stop position to a function.
7. Write a test that has coverage for selecting different stack frames. 

co-authored-by: Remco Smits \<djsmits12@gmail.com\>

Release Notes:

- N/A

---------

Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-07 12:39:35 +00:00
Anthony Eid
7bc3f74cab Fix panel button context menu overlap with tooltip hint (#30108)
This fix works by disabling the tooltip whenever the menu is being
rendered.

### Before

![image](https://github.com/user-attachments/assets/0b275931-fd1f-4748-88dd-cb76a52f8810)
### After

![image](https://github.com/user-attachments/assets/7260eb48-3a24-43ee-8df7-b6c48e8a4024)


Release Notes:

- Fix panel button tooltip overlapping with the panel button's right
click menu
2025-05-07 12:33:13 +00:00
Conrad Irwin
02765947e0 Use the console for errors too (#29992)
Release Notes:

- N/A
2025-05-07 13:26:08 +01:00
Kirill Bulatov
f7e77123cc Do not flicker when switching cmd-hovered words in terminal (#30098)
Closes https://github.com/zed-industries/zed/issues/25110


https://github.com/user-attachments/assets/4624c256-8dfb-48eb-a726-6cf130d946da

Terminal may update its hovered word way before reporting it to the
terminal view, and that processing the file check later.
Hence, store the terminal hover data in the terminal view and avoid
highlights when it's different from what the terminal has (as the source
of truth here).

In addition, now only does hover refreshes when the terminal hover
actually changes, not on every event report.

Release Notes:

- Fixed underline flicker when switching cmd-hovered words in terminal
2025-05-07 11:04:11 +00:00
Ben Brandt
c19a5c2fd6 Revert "Stop generating in the Agent panel when the user edits a previous message (#29915)" (#30092)
This reverts commit ce053c9bff.

Closes #ISSUE

Release Notes:

- N/A
2025-05-07 09:57:20 +00:00
Antonio Scandurra
9711fb49dc Fix zero-sized message editors when context strip is empty (#30079)
Release Notes:

- Fixed a bug that would cause the message composer in the agent panel
to not render when the context strip was empty.

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-05-07 09:02:20 +00:00
Oleksiy Syvokon
c15e5d275a docs: Fix links to /ai/configuration (#30069)
Release Notes:

- N/A
2025-05-07 06:33:44 +00:00
Oleksiy Syvokon
858d61a65e docs: Update old /assistant links (#30067)
Take 2

Release Notes:

- N/A
2025-05-07 06:12:10 +00:00
Ron Harel
1fcd2647ed workspace: Fix inactive pane dimming (#29473)
Closes #27173

Problem:

Active panes nested within axes were incorrectly receiving opacity
overlays, while inactive panes in nested structures would get multiple
overlays applied, making them appear darker than intended.

Solution:

I fixed this by distinguishing between leaf panes and axes in the
rendering pipeline, applying overlays only to elements that are both
leaf panes and not active, ensuring consistent visual treatment
regardless of their position in the hierarchy.

Release Notes:

- Fixed an issue where `inactive_opacity` settings would be applied to
panes multiple times and even to the active pane when nested within
another pane.
2025-05-07 09:02:34 +03:00
Oleksiy Syvokon
60d51d56cd docs: fix redirects and update old /assistant links (#30065)
Release Notes:

- N/A
2025-05-07 05:52:36 +00:00
Cole Miller
03b635bb27 Avoid panic when opening thread as markdown in non-local project (#30061)
Right now `agent: open active thread as markdown` will always panic when
you try to use it over collab or when SSH remoting. This PR makes it log
an error instead (we should follow up by restoring full remote support).

Release Notes:

- Prevented `agent: open active thread as markdown` from panicking when
used in a non-local project.
2025-05-07 01:15:45 -04:00
Danilo Leal
f7511c3f65 docs: Follow up tweaks to AI docs (#30060)
Follow up to https://github.com/zed-industries/zed/pull/29747

Release Notes:

- N/A
2025-05-07 01:42:54 -03:00
Agus Zubiaga
264097e253 agent: Use correct timezone for thread history separators (#30059)
Turns out `naive_local` doesn't actually offset a `DateTime<Utc>` to the
local timezone before creating a `NaiveDate`.

Release Notes:

- agent: Use correct timezone for thread history separators
2025-05-07 04:41:04 +00:00
Danilo Leal
795fadc0bc docs: Overhaul AI documentation (#29747)
To support the Agentic Editing launch. To dos before merging:

- [ ] Anything marked as `todo!` within `docs/src` (Anyone)
- [x] Check all internal links (Joe)
- Joe: I checked all links and fixed all aside from a few that I
annotated with `todo!` comments
- [ ] Update images (Danilo)
- [ ] Go over / show images of tool cards in agent panel overview
(Danilo)
- [ ] Point billing FAQ to new billing docs (Joe)
- [x] Redirects external links
    - [ ] Needs testing
- [x] Delete old docs
- [ ] Ensure all mentioned bindings use the `{#kb ...}` format and that
they are rendering correctly
- [ ] All agent-related actions are now `agent::` and not `assistant::`
- [x] Mention support of `.rules` files in `rules.md`

Release Notes:

- N/A

---------

Co-authored-by: Joseph T. Lyons <josephtlyons@gmail.com>
Co-authored-by: morgankrey <morgankrey@gmail.com>
Co-authored-by: Smit Barmase <37347831+smitbarmase@users.noreply.github.com>
Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-07 01:07:12 -03:00
Marshall Bowers
38975586d4 agent: Tweak onboarding modal copy (#30057)
This PR tweaks the copy for the Agent's onboarding modal.

Release Notes:

- N/A
2025-05-07 02:02:13 +00:00
Marshall Bowers
5539d82ea6 agent: Remove feature flag checks (#30055)
This PR removes all of the feature flag checks related to the Agent.

Tried to do this in the least invasive way possible; we can follow up
with a full removal.

Release Notes:

- N/A
2025-05-06 21:38:05 -04:00
Mikayla Maki
0cdd8bdded Restore tool cards on thread deserialization (#30053)
Release Notes:

- N/A

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-05-06 18:16:34 -07:00
Joseph T. Lyons
ab3e5cdc6c Bump Zed to v0.187 (#30052)
Release Notes:

-N/A
2025-05-07 00:03:16 +00:00
Marshall Bowers
28e664c433 agent: Launch it (#30005)
This PR enables the Agent-related feature flags on the client.

Release Notes:

- N/A
2025-05-06 19:41:32 -04:00
Marshall Bowers
300da3b718 Add an onboarding banner for the Agent panel (#30050)
This PR adds an onboarding banner for the Agent panel:

<img width="262" alt="Screenshot 2025-05-06 at 6 54 58 PM"
src="https://github.com/user-attachments/assets/52849e64-7d5d-488c-8456-4d7bd97f8ebd"
/>

Release Notes:

- N/A
2025-05-06 23:41:04 +00:00
Marshall Bowers
0db8668404 git_ui: Fix resetting of onboarding banner (#30051)
This PR fixes an issue where the Git onboarding banner wasn't able to be
reset.

Release Notes:

- N/A
2025-05-06 23:20:37 +00:00
Michael Sloan
ffc07a2651 Use agent panel font size for all content in thread / history views and fix text thread font size adjust (#30041)
Release Notes:

- N/A
2025-05-06 23:15:58 +00:00
Piotr Osiewicz
bbffe1ec2c debugger: Unify landing state for new session modal (#30046)
Closes #ISSUE

Release Notes:

- N/A
2025-05-07 00:27:50 +02:00
Marshall Bowers
cec1d2584b collab: Don't transfer existing usage when upgrading to Zed Pro (#30045)
This PR makes it so we don't transfer existing usage over when upgrading
from a trial to Zed Pro.

Release Notes:

- N/A
2025-05-06 17:29:06 -04:00
Agus Zubiaga
3cdf5ce947 agent: Allow customizing temperature by provider/model (#30033)
Adds a new `agent.model_parameters` setting that allows the user to
specify a custom temperature for a provider AND/OR model:

```json5
    "model_parameters": [
      // To set parameters for all requests to OpenAI models:
      {
        "provider": "openai",
        "temperature": 0.5
      },
      // To set parameters for all requests in general:
      {
        "temperature": 0
      },
      // To set parameters for a specific provider and model:
      {
        "provider": "zed.dev",
        "model": "claude-3-7-sonnet-latest",
        "temperature": 1.0
      }
    ],
```

Release Notes:

- agent: Allow customizing temperature by provider/model

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-06 20:36:25 +00:00
Mikayla Maki
0055a20512 Remember max mode setting per-thread and add a user setting (#30042)
Supersedes: https://github.com/zed-industries/zed/pull/29936

Thanks for your contribution @imumesh18, but we had a slightly different
take on it :)

Release Notes:

- N/A

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-06 20:11:21 +00:00
Marshall Bowers
6bb6e48171 agent: Only show the trial upsell in the thread view (#30040)
This PR makes it so we only show the trial upsell in the thread view.

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

Release Notes:

- Agent Beta: Changed the trial upsell to only be visible in the thread
view.
2025-05-06 19:47:51 +00:00
Nathan Sobo
91cfce09bc Clean up some styling issues in the editing tool card and render the animated lines a bit smaller (#30038)
Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-06 19:31:50 +00:00
Cole Miller
9d1604b674 agent: Add missing Linux keybindings (#30032)
This PR updates the default Linux keybindings to align with changes made
to the macOS bindings in #29943.

Release Notes:

- N/A
2025-05-06 15:23:52 -04:00
Mikayla Maki
0fdc04532a Fix token count not appearing for the first message (#30035)
Release Notes:

- N/A

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-06 18:57:22 +00:00
Marshall Bowers
5002156e32 ci: Add check for formatting default.json (#30034)
This PR adds a check in CI to ensure that `assets/settings/default.json`
is formatted consistently.

Release Notes:

- N/A
2025-05-06 18:55:26 +00:00
anteater
bd11bb5409 Add setting to hide onboarding banners (#29709)
Closes #28637 aka #29219.

Release Notes:

- Added `workspace.title_bar.show_onboarding_banner` preference to hide
onboarding banners.
- Relocated `workspace.show_user_picture` preference to
`workspace.title_bar.show_user_picture`.
2025-05-06 18:54:09 +00:00
Antonio Scandurra
c92b2e31e1 Avoid panicking when edit agent emits an empty old_text tag (#30030)
Release Notes:

- Fixed a panic that could sometimes occur when the agent applies edits.

Co-authored-by: Nathan <nathan@zed.dev>
2025-05-06 18:20:10 +00:00
Piotr Osiewicz
09d3ff9dbe debugger: Rework language association with the debuggers (#29945)
- Languages now define their preferred debuggers in `config.toml`.
- `LanguageRegistry` now exposes language config even for languages that
are not yet loaded. This necessitated extension registry changes (we now
deserialize config.toml of all language entries when loading new
extension index), but it should be backwards compatible with the old
format. /cc @maxdeviant

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
2025-05-06 20:16:41 +02:00
Marshall Bowers
544e8fc46c agent: Don't render trial upsell when not using the Zed provider (#30029)
This PR makes it so we don't render the trial upsell when not using the
Zed provider.

Release Notes:

- Agent Beta: Changed Zed Pro trial upsell to only be displayed when
using a model through the Zed provider.
2025-05-06 18:07:25 +00:00
Cole Miller
b83d00d69b agent: Fix "tool cancelled" status being overapplied to failed tool calls (#30021)
Release Notes:

- Agent Beta: Fixed a bug that caused past failed tool calls to
incorrectly display as cancelled by the user.
2025-05-06 17:53:00 +00:00
Marshall Bowers
7a9165d5ce agent: Don't render usage callouts when not using the Zed provider (#30025)
This PR makes it so we don't render the usage callouts when not using
the Zed provider.

Release Notes:

- Agent Beta: Changed usage callouts to only be displayed when using a
model through the Zed provider.
2025-05-06 17:50:26 +00:00
Bennet Bo Fenner
80236d0bb9 agent: Handle context servers that do not provide a configuration in MCP setup dialog (#30023)
<img width="674" alt="image"
src="https://github.com/user-attachments/assets/0ccb89e2-1dc1-4caf-88a7-49159f43979f"
/>
<img width="675" alt="image"
src="https://github.com/user-attachments/assets/790e5d45-905e-45da-affa-04ddd1d33c65"
/>

Release Notes:

- N/A
2025-05-06 17:18:49 +00:00
Umesh Yadav
a743035286 lmstudio: Fix streaming not working in v0.3.15 (#30013)
Closes #29781

Tested this with llama3, gemma3 and qwen3.

This is a breaking change, which means after adding this code changes in
future version zed we will require atleast lmstudio >= 0.3.15. For
context why it's breaking changes check out the issue: #29781.

What this doesn't try to solve is:

* Tool calling, thinking text rendering. Will raise a seperate PR for
these as those are not required in this PR to make it work.


https://github.com/user-attachments/assets/945f9c73-6323-4a88-92e2-2219b760a249

Release Notes:

- lmstudio: Fixed Zed support for LMStudio >= v0.3.15 (breaking change -- older versions are no longer supported).

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 12:59:36 -04:00
Piotr Osiewicz
bbfcd885ab debugger: Allow locators to generate full debug scenarios (#30014)
Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
2025-05-06 18:39:49 +02:00
Marshall Bowers
a378b3f300 collab: Treat staff as having usage-based pricing enabled (#30020)
This PR makes it so staff are treated as having opted-in to usage-based
pricing.

Release Notes:

- N/A
2025-05-06 16:06:03 +00:00
Cole Miller
6d2c39c265 Fix checkpoints not being rendered (#30019)
Closes #ISSUE

Release Notes:

- Agent Beta: Fixed a bug causing "Restore Checkpoint" buttons in the
agent panel not to be rendered.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-06 15:59:06 +00:00
Cole Miller
1a80103eaf Silence error log when deserializing agent panel navigation history (#30011)
Closes #ISSUE

Release Notes:

- N/A
2025-05-06 11:46:23 -04:00
Marshall Bowers
6cb436565f collab: Disable usage-based billing thresholds (#30016)
This PR disables the usage-based billing thresholds.

Release Notes:

- N/A
2025-05-06 15:21:22 +00:00
Kirill Bulatov
007fd0586a Adds a way to dismiss workspace notifications (#30015)
Closes https://github.com/zed-industries/zed/issues/10140

* On `menu::Cancel` action (`ESC`), close notifications, one by one, if
`Workspace` gets to handle this action.
More specific, focused items contexts (e.g. `Editor`) take priority.

* Allows to temporarily suppress notifications of this kind either by
clicking a corresponding button in the UI, or using
`workspace::SuppressNotification` action.

This might not work well out of the box for all notifications and might
require further improvement.


https://github.com/user-attachments/assets/0ea49ee6-cd21-464f-ba74-fc40f7a8dedf


Release Notes:

- Added a way to dismiss workspace notifications
2025-05-06 18:15:26 +03:00
Cole Miller
7d361ec97e Fall back to old key when loading agent settings (#30001)
This PR updates #29943 to fall back to loading agent panel settings from
the old `assistant` key if the `agent` key is not present. Edits to
these settings will also target `assistant` in this situation instead of
`agent` as before.

Release Notes:

- Agent Beta: Fixed a regression that caused the agent panel not to
load, or buttons in the agent panel not to work.
2025-05-06 14:31:38 +00:00
drathier
a9d5b2064e docs: Add link to formatter settings from configuring-languages (#29981)
Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 10:29:58 -04:00
Antonio Scandurra
0f50e6b1d1 Fix error when requesting completion to Copilot Chat without tools (#30007)
The API will return a Bad Request (with no error message) when tools
were used previously in the conversation but no tools are provided as
part of a new request.

Inserting a dummy tool seems to circumvent this error.

Release Notes:

- Fixed an error that could sometimes occur when editing using Copilot
Chat.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-05-06 14:19:59 +00:00
Marshall Bowers
fed5f89f8d agent: Add enabled indicator in Max Mode tooltip (#30008)
This PR adds an enabled indicator in the Max Mode tooltip to show when
it is enabled:

<img width="409" alt="Screenshot 2025-05-06 at 9 49 48 AM"
src="https://github.com/user-attachments/assets/43d3f6dd-5658-467a-9df9-606ce326426a"
/>

Release Notes:

- Agent Beta: Added an indicator in the Max Mode tooltip to show when it
is enabled.

Co-authored-by: Danilo <danilo@zed.dev>
2025-05-06 14:07:31 +00:00
Marshall Bowers
096355915a agent: Add label to Max Mode toggle (#30003)
This PR adds a label to the Max Mode toggle, for increased clarity:

<img width="647" alt="Screenshot 2025-05-06 at 9 16 35 AM"
src="https://github.com/user-attachments/assets/38cd55fb-43ad-430b-8b4c-5adf707317cf"
/>

Release Notes:

- Agent Beta: Added a label to the Max Mode toggle.
2025-05-06 09:40:20 -04:00
Bennet Bo Fenner
e44367c6d0 agent: Disable claude-3-7-sonnet-thinking tool support for Copilot Chat (#29999)
We started getting Bad Requests from the Copilot Chat API.
Seems like Microsoft stopped supporting this:
<img width="331" alt="image"
src="https://github.com/user-attachments/assets/46050063-f031-4836-82ff-219bdd45639a"
/>


Release Notes:

- agent: Disable `claude-3-7-sonnet-thinking` for Copilot Chat Provider
because it is not supported by Copilot Chat

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-06 12:47:26 +00:00
Antonio Scandurra
07e6e49583 Add new editing eval scenario and improve it substantially (#29997)
This improves the new eval scenario by ~80% (`0.29` vs `0.525`) without
decreasing performance in the other evals.

Release Notes:

- Improved the performance of the `edit_file` tool.
2025-05-06 12:22:42 +00:00
Fernando Tagawa
6e9f8f997e markdown: Ignore html comments (#28318)
Closes #28300

| Before | After |
| ------ | ----- |
|
![Screenshot_20250408_073355](https://github.com/user-attachments/assets/50dcb56d-bc70-4329-94cb-5b848f265c97)
|
![Screenshot_20250408_073322](https://github.com/user-attachments/assets/ba5c519a-bb34-4724-9c14-3278c6c09afd)
|

Release Notes:

- N/A
2025-05-06 14:55:07 +03:00
Ben Brandt
daba603e27 agent: Fix Open Thread as Markdown not working when another panel is focused (#29993)
Release Notes:

- agent: Fix Open Thread as Markdown not working when another panel is
focused

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-05-06 12:56:01 +02:00
Oleksiy Syvokon
ac007139ab evals: Enable Python LSP (#29987)
We now have one eval that uses a Python repo


Release Notes:

- N/A
2025-05-06 10:28:59 +00:00
Conrad Irwin
68793c0ac2 Debug adapters log to console (#29957)
Closes #ISSUE

Release Notes:

- N/A
2025-05-06 11:21:34 +01:00
Agus Zubiaga
de554589a8 agent: Add date separators to Thread History (#29961)
Adds time-bucket separators to the thread history list:


https://github.com/user-attachments/assets/c9ac3ec4-b632-4ea5-8234-382b48de2bd6

Note: I'm simulating that Today is next Thursday so that I can show the
"This Week" bucket.

Release Notes:

- agent: Add date separators to Thread History
2025-05-06 10:18:48 +00:00
Conrad Irwin
4fdd14c3d8 Remove another unwrap on regex compilation (#29984)
Follow up to #29979

Release Notes:

- Fixed a (hypothetical) panic in terminal search
2025-05-06 11:18:03 +01:00
Murt
848c4f77a6 fix(vim): Store up to the 9th numbered register instead of 7th (#29986)
Release Notes:

- Fixed an issue where we only automatically stored 7 numbered registers
instead of 9
2025-05-06 11:17:45 +01:00
Finn Evers
06794f35bc assistant: Do not create new context on load (#29480)
Closes https://github.com/zed-industries/zed/issues/27673
Closes https://github.com/zed-industries/zed/issues/29344
Closes #29863 

This PR fixes an issue where Zed was showing no language and `4:1` as a
line/column value on startup, as described in the linked issues. You can
actually see in the first issue that the user also experiences the same
issue as described in the second one, as his line/column value is
noticably also `4:1`.


https://github.com/user-attachments/assets/bb60e387-f4b8-4e05-80b3-4dadf1a01262

This issue arises because on assistant panel load, a new context is
created and its editor focused. However, the editor is not visible
despite having focus. The content for the editor for a new context is
`\n\n\n` and the cursor is inserted directly after that - this is where
the line:column position `4:1` comes from. For the assistant panel
editor, the language is intentionally hidden, this is why the language
is not shown on workspace load.
The issue is only present for as long as the user does not focus and
edit another editor, then that instance is focused and everything starts
to work properly again.

As this issue only arises with the old assistant panel, some staff
members were unable to reproduce in the linked issues. Once you set
`export ZED_DISABLE_STAFF=1` in your environment, you should also be
able to reproduce this issue consistently.

--- 

This PR fixes the issue by not creating a new context on assistant panel
load. This should not cause any regressions; every other code path I
checked creates a new context if no context is yet present.
Additionally, this also seems somewhat more reasonable, as users which
have the assistant panel disabled will never need a new context anyway,
so no context should be created.

In the following video, you can see this fixes the issue when the
assistant panel was not open the last time Zed was opened. If the panel
was open before Zed was closed, we will still properly focus the panel
and then the `4:1` will show again, which in that case is correct. The
assistant panel editor is focused and the missing language as well as
the line number then match what the user sees, experiences and expects.


https://github.com/user-attachments/assets/224a786b-52c7-4212-bccb-dff6d9db62c3


Release Notes:

- Fixed an issue where Zed would show no language and an incorrect
line/column value on startup.

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 11:38:08 +02:00
Conrad Irwin
ef31252ef8 Fix panic in update_selection_occurrence_highlights (#29979)
Closes #ISSUE

Release Notes:

- Fixed a (rare) panic when highlighting text in the editor
2025-05-06 10:23:51 +01:00
Smit Barmase
5640265160 language: Fix larger syntax node when cursor is at end of word or line (#29978)
Closes #28699

Fixes two cases in the `editor::SelectLargerSyntaxNode` action:
1. When cursor is at the end of a word, it now selects that word first
instead of selecting the whole line.
2. When cursor is at the end of a line, it now selects that line first
instead of selecting the whole code block.

Before and After:


https://github.com/user-attachments/assets/233b891e-15f1-4f10-a51f-75693323c2bd

Release Notes:

- Fixed `editor::SelectLargerSyntaxNode` to properly select nodes when
the cursor is positioned at the end of words or lines.
2025-05-06 14:43:28 +05:30
Shashank Verma
9d97e08e4f title_bar: Add icon for project branch trigger button (#29494)
Added icon for branch switcher in title bar

| `main`    | This PR |
| -------- | ------- |
| <img width="196" alt="Screenshot 2025-04-27 at 1 02 47 PM"
src="https://github.com/user-attachments/assets/5625f6c5-7b11-4f3d-bed8-6ea3b74d9416"
/> | <img width="217" alt="Screenshot 2025-04-27 at 1 07 11 PM"
src="https://github.com/user-attachments/assets/6c83daa6-fa71-44a8-8f6b-e33b2217b29e"
/> |

Release Notes:

- Added icon for branch switcher in title bar

---------

Signed-off-by: Shashank Verma <shashank.verma2002@gmail.com>
2025-05-06 08:38:24 +00:00
tidely
6b37646179 client: Implement Socks identification and authorization (#29591)
Closes #28792 

supersedes #28854 

- Adds support for Socks V4 Identification using a userid, and
Authorization using a username and password on Socks V5.
- Added tests for parsing various Socks proxy urls.
- Added a test for making sure a misconfigured socks proxy url doesn't
expose the user by connecting directly as a fallback.

Release Notes:

- Added support for identification and authorization when using a sock
proxy
2025-05-06 08:03:56 +00:00
Aaron Feickert
da3a696a60 editor: Remove extra quotes from outline search term (#29829)
The outline panel includes quotes around search terms. The rendering
makes it somewhat ambiguous whether these quotes are part of the search
term and are unnecessary, especially given other rendering
differentiation. This PR removes them.

Release Notes:

- N/A
2025-05-06 10:50:33 +03:00
Finn Evers
6bacea28bc editor: Do not insert scrollbar hitboxes when scrollbars are never to be shown (#29316)
This PR fixes an issue where scrollbar hitboxes were still inserted for
editors despite scrollbars being programmatically disabled via the
`show_scrollbars`field. This is basically the same fix as in #27467.

The thought process here is that the motivation for `show_scrollbars` is
not to just hide the scrollbars in the editor, but to fully disable
scrollbars for the associated editor. However, this is currently not the
case, as a functioning hitbox for each scrollbar is stil inserted. For
example, the behavior with the old assistant panel can be seen below:


https://github.com/user-attachments/assets/18af6338-dd28-4794-a6a6-5b9691b243f2

Whilst the scrollbar is not visible, there is still a scrollbar hitbox
inserted which triggers hover events and is fully functioning.


This PR fixes this by fully skipping the scrollbar layouting whenever
`show_scrollbars` is set to false, preventing the hitboxes from being
inserted.


https://github.com/user-attachments/assets/b6bb6dc7-902f-4383-bf03-506d0a57ec77


Release Notes:

- N/A
2025-05-06 10:25:01 +03:00
Finn Evers
3b90d62bb2 editor: Implement hover color for scrollbars (#28064)
This PR adds hover colors to the editor scrollbars:


https://github.com/user-attachments/assets/6600810e-7e8e-4dee-9bef-b7be303b5fe0

The color used here is the existing `scrollbar_thumb_hover_background`
color provided by themes.

Looking forward to feedback 😄 

Release Notes:

- Added hover state to editor scrollbars.
2025-05-06 10:17:43 +03:00
Ivan Banov
55a0bb2a91 Add default tab_size for Elm (#29547)
This PR updates the default tab size to 4 spaces, aligning with the
standard adopted by the Elm community and the official language
formatter (elm-format).

Reference: [elm-format tab size
default](https://github.com/avh4/elm-format/blob/main/elm-format-lib/src/Box.hs#L249)
2025-05-06 10:14:02 +03:00
Antonio Scandurra
210c338df4 Restore original file content when rejecting an overwritten file (#29974)
Release Notes:

- Fixed a bug that would cause rejecting a hunk from the agent to delete
the file if the agent had decided to rewrite that file from scratch.
2025-05-06 07:05:55 +00:00
neunato
86cc5c2b55 Apply autoscroll_on_clicks when extending selection (#28235)
Closes https://github.com/zed-industries/zed/issues/22240

Release Notes:

- Fixed `autoscroll_on_clicks` not being applied when expanding
selection

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-06 07:05:08 +00:00
Anthony Eid
a0bfe4d293 debugger: Fix debug scenario's defined in debug.json not using passed in build task (#29973)
There were two bugs that caused user-defined debug scenarios from being
able to run a build task.

1. DebugRequest would be deserialized to `Attach` even when `process_id`
wasn't defined in a user's configuration file. This has been fixed by
adding our own deserializer that defaults to None if there are no fields
present instead of `Attach`, and I added tests to prevent regressions.
2. Debug scenario resolve phase never got the active buffer when
spawning a debug session from the new session modal. This has been
worked around by passing in the worktree_id of a debug scenario in the
scenario picker and the active worktree_id otherwise.

Release Notes:

- N/A
2025-05-06 08:54:57 +02:00
neunato
52ea501f4f Fix multicursors not being added when clicking on line numbers (#28263)
Closes https://github.com/zed-industries/zed/issues/21372

Release Notes:

- Fixed multicursors not being added when clicking on line numbers

-----

I tracked this down to
b6ee367ee0/crates/editor/src/element.rs (L591)

being forwarded to `editor.select()` a few lines below
b6ee367ee0/crates/editor/src/element.rs (L667-L675)

resulting in `add == true` and `click_count == 3`, triggering this
b6ee367ee0/crates/editor/src/editor.rs (L2750-L2752)

... and we end up removing the previous selection. 

This was added [in
2021](bfecdb7bc0)
under this reasoning:

> This prevents selections added in earlier clicks from being rendered
under the pending selection.

which no longer seems to be an issue, so removing should be safe?

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 09:45:43 +03:00
Francisco Fernandes
a07ba3c718 editor: Fix inconsistent SelectPrevious behavior (#27695)
When starting a selection from only carets, the action
`editor::SelectPrevious` behaved in a manner inconsistent with
`editor::SelectNext` as well as equivalent keybinds in editors such as
VSCode, by selecting substrings of whole words matching the initially
selected string on subsequent triggers.

This fix brings the `select_previous` function in line with
`select_next_internal`by calling `select_match_ranges` (previously an
internal function of `select_next_internal`) in the same way it was
previously used in the function that exhibited expected behavior.

Furthermore, the relevant test was adapted to bring it in line with the
equivalent test for the `editor::SelectNext` action

Closes #24346

Release Notes:

- Fixed inconsistent SelectPrevious behavior
2025-05-06 09:37:58 +03:00
Max Brunsfeld
2eb10ab9fb openai: Don't append tool calls to prior assistant messages (#29969)
Closes https://github.com/zed-industries/zed/issues/29821

Release Notes:

- Fixed an issue in the agent panel where OpenAI requests would fail if
the assistant begins its response with a tool call.
2025-05-05 22:04:56 -07:00
Eva Pace
55fd8352e4 assistant_slash_commands: Be more precise in content type matching (#29124)
While investigating https://github.com/zed-industries/zed/issues/28076,
I found out often times the content type header of a website comes with
more data, such as the `charset`. So instead of doing an equal
comparison, I changed to a `starts_with`.

You can see an example here:

```shell
$ curl -sS -D - https://github.com/zed-industries/zed/blob/main/Cargo.toml -o /dev/null | head -n 10
HTTP/2 200
date: Sun, 20 Apr 2025 10:19:52 GMT
content-type: text/html; charset=utf-8
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame,Accept-Encoding, Accept, X-Requested-With
etag: W/"92dabf048b34d04a1b1d94e29cae4aca"
cache-control: max-age=0, private, must-revalidate
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
```

Release Notes:

- Improved Content Type matching of `/fetch` commands in Assistant

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-05-06 04:38:03 +00:00
Michael Sloan
0b10eb7577 Remove Tree-sitter AST logging from SelectLargerSyntaxNode (#29949)
Release Notes:

- N/A
2025-05-06 04:30:06 +00:00
Jason Lee
3d737fd268 gpui: Update argument name of the from_corners method (#29968)
Release Notes:

- N/A
2025-05-06 07:17:39 +03:00
Nate Butler
c5d8407df4 component: Component crate cleanup (#29967)
This PR further organizes and documents the component crate. It:

- Simplifies the component registry
- Gives access to `ComponentMetadata` sooner
- Enables lookup by id in preview extension implementations
(`ComponentId` -> `ComponentMetadata`)
- Should slightly improve the performance of ComponentPreview

It also brings component statuses to the Component trait:

![CleanShot 2025-05-05 at 23 27
11@2x](https://github.com/user-attachments/assets/dd95ede6-bc90-4de4-90c6-3e5e064fd676)

![CleanShot 2025-05-05 at 23 27
40@2x](https://github.com/user-attachments/assets/9520aece-04c2-418b-95e1-c11aa60a66ca)

![CleanShot 2025-05-05 at 23 27
57@2x](https://github.com/user-attachments/assets/db1713d5-9831-4d00-9b29-1fd51c25fcba)

Release Notes:

- N/A
2025-05-06 03:41:52 +00:00
Danilo Leal
377909a646 Fix toolbar spacing regressions (#29964)
Cleaning up as I introduced a few regressions in this PR:
https://github.com/zed-industries/zed/pull/29866.

Release Notes:

- N/A
2025-05-05 22:28:35 -03:00
Cole Miller
bdd911f89e Update assistant to agent in settings and keymaps (#29943)
Closes #ISSUE

Release Notes:

- Agent Beta: Renamed the top-level `assistant` settings key to `agent`.
A migration for existing settings files is included.
- Agent Beta: Moved the `assistant::ToggleFocus`,
`assistant::ToggleModelSelector`, and `assistant::OpenRulesLibrary`
actions to the `agent` namespace. Existing keymaps that mention these
actions by their old names will continue to work.

---------

Co-authored-by: Max <max@zed.dev>
2025-05-06 01:02:56 +00:00
Max Brunsfeld
34e10e4e56 Honor the prompt field of inline assist action (#29960)
Closes https://github.com/zed-industries/zed/issues/29337

Release Notes:

- Fixed a bug where the `prompt` field was ignored on custom key
bindings for `InlineAssist`

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-05 17:24:31 -07:00
Max Brunsfeld
275c808b03 Allow dragging files and tabs into the agent panel (#29959)
Release Notes:

- Added the ability to drag files and tabs onto the new agent panel.

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-06 00:21:22 +00:00
Mikayla Maki
b214c9e4a8 Fix profile menu hover flickering due to documentation asides (#29958)
Fixes https://github.com/zed-industries/zed/issues/29909 

🍐'd with @nathansobo 

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2025-05-05 23:05:47 +00:00
Bennet Bo Fenner
2aa06d1d0f agent: Switch to new web search provider (#29951)
Release Notes:

- N/A
2025-05-06 00:47:11 +02:00
Nate Butler
9568fa1166 Add Zed Pro Trial Upsell (#29938)
This PR adds an upsell to try Zed Pro

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-05 18:46:54 -04:00
xdBronch
b4653c15b8 language: Add fallback for enum member completion highlight (#27929)
i tried to use `variant` but it wasnt giving any color despite my theme
definitely having a color for it, am i doing something wrong? i think
`property` is an alright fallback
before:

![image](https://github.com/user-attachments/assets/e020ec4d-3a85-45fb-9ddb-823c55e0afca)
after:

![image](https://github.com/user-attachments/assets/3c335ed6-746e-4136-858a-8b80e5229f29)


Release Notes:

- N/A
2025-05-05 22:25:09 +00:00
Nathan Sobo
4896e0bc02 Allow the agent panel font size to be customized (#29954)
You can set `agent_font_size` as a top-level settings key. You can also
use `zed::IncreaseBufferFontSize` and `zed::DecreaseBufferFontSize` and
`zed::ResetBufferFontSize` the agent panel is focused via the standard
bindings to adjust the agent font size. In the future, it might make
sense to rename these actions to be more general since "buffer" is now a
bit of a misnomer. 🍐'd with @mikayla-maki

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-05-05 16:13:14 -06:00
Noritada Kobayashi
0bf682a0d5 docs: Fix a broken link to the PyRight Settings section (#29283)
This PR fixes a broken link to the PyRight Settings section.
This is a follow-up to 5f390f1bf8.

Release Notes:

- N/A
2025-05-05 18:02:53 -04:00
Max Brunsfeld
3d0c4d716d Use the same context store for all inline assists in a project (#29953)
Release Notes:

- Made context attachments in inline assist prompts persist across
inline assist invocations.

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-05 22:01:02 +00:00
Marshall Bowers
b6c7df8183 inline_completion_button: Show the initial usage data from the server (#29952)
This PR updates the usage indicator for edit predictions to show the
initial usage data returned from the server.

Release Notes:

- N/A
2025-05-05 21:39:49 +00:00
Anthony Eid
1aa92d9928 debugger: Enable setting debug panel dock position to the side (#29914)
### Preview
<img width="301" alt="Screenshot 2025-05-05 at 11 08 43 PM"
src="https://github.com/user-attachments/assets/aa445117-1c1c-4d90-a3bb-049f8417eca4"
/>


Setups the ground work to write debug panel persistence tests and allows
users to change the dock position of the debug panel.


Release Notes:

- N/A
2025-05-05 21:27:20 +00:00
Pavel
6e28400e17 gpui: Fix a bug with Japanese romaji typing in input example (#28507)
Steps to reproduce:
* On macOS, run `input` example
* type `aaa|bbb` place caret on the place marked with |
* switch to `japanese romaji`
* press `ko`
* press left arrow

<img width="412" alt="image"
src="https://github.com/user-attachments/assets/d3c02e9b-98f9-420e-a3b7-681ba90829cd"
/>

You will get `aaa` duplicated with every arrow press.

According to [reference
implementation](https://developer.apple.com/library/archive/samplecode/TextInputView/Listings/FadingTextView_m.html#//apple_ref/doc/uid/DTS40008840-FadingTextView_m-DontLinkElementID_6)
we need to unmark text when we get empty line in `setMarkedText `
2025-05-06 00:15:41 +03:00
Lorenzo Lewis
78545a93ea gpui: Fix typo in doc comment (#29950)
Fixes a typo in gpui docs

Release Notes:

- N/A
2025-05-05 17:05:48 -04:00
Max Brunsfeld
dd79c29af9 Allow attaching text threads as context (#29947)
Release Notes:

- N/A

---------

Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-05-05 13:59:21 -07:00
chbk
7f868a2eff Improve Rust macro highlighting (#28182)
Release Notes:

  - Improved Rust macro highlighting

| Zed 0.180.2 | With this PR |
| --- | --- |
|
![Image](https://github.com/user-attachments/assets/013c73b1-5eee-45b1-ba37-747563c1bc4b)
|
![Image](https://github.com/user-attachments/assets/57eb97e3-1ccc-4d58-9596-bb3decedc0f4)
|

```rust
macro_rules! square {
  ($e:expr) => { $e * $e };
}
```

- `$var`: `variable`
- `expr`: `type`
2025-05-05 23:50:57 +03:00
Conrad Irwin
6497aa5341 Show request in evaluate selection command (#29621)
Closes #ISSUE

Release Notes:

- N/A
2025-05-05 21:32:00 +01:00
Nathan Sobo
55b908a8bf Allow agent edits to be accepted/rejected before the end the turn (#29941)
Release Notes:

- N/A
2025-05-05 14:25:34 -06:00
Conrad Irwin
ff215b4f11 debugger: Run build in terminal (#29645)
Currently contains the pre-work of making sessions creatable without a
definition, but still need to change the spawn in terminal
to use the running session

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-05-05 20:08:14 +00:00
Cole Miller
c12e6376b8 Terminal tool improvements (#29924)
WIP

- On macOS/Linux, run the command in bash instead of the user's shell
- Try to prevent the agent from running commands that expect interaction

Release Notes:

- Agent Beta: Switched to using `bash` (if available) instead of the
user's shell when calling the terminal tool.
- Agent Beta: Prevented the agent from hanging when trying to run
interactive commands.

---------

Co-authored-by: WeetHet <stas.ale66@gmail.com>
2025-05-05 15:57:03 -04:00
Bennet Bo Fenner
9cb5ffac25 context_store: Refactor state management (#29910)
Because we instantiated `ContextServerManager` both in `agent` and
`assistant-context-editor`, and these two entities track the running MCP
servers separately, we were effectively running every MCP server twice.

This PR moves the `ContextServerManager` into the project crate (now
called `ContextServerStore`). The store can be accessed via a project
instance. This ensures that we only instantiate one `ContextServerStore`
per project.

Also, this PR adds a bunch of tests to ensure that the
`ContextServerStore` behaves correctly (Previously there were none).

Closes #28714
Closes #29530

Release Notes:

- N/A
2025-05-05 21:36:12 +02:00
Oleksiy Syvokon
8199664a5a agent: Handle attempts to use hallucinated tools (#29946)
This change:

1. Catches attempts to use missing tools. If this happens, we now send
Agent a message listing available tools, after which Agent can
gracefully recover. Prior behavior: thread would stop in a broken state.

Example of a hallucinated call and a message we send back: 

![image](https://github.com/user-attachments/assets/92a8f700-b192-4038-8c7e-0a74ca2e0146)

2. Adds evals for hallucinated tool use and imagined edits
3. Adds ability to configure a profile name in evals.



Release Notes:

- N/A
2025-05-05 19:31:11 +00:00
Danilo Leal
7dfbe0b908 agent: Improve terminal tool card design (#29712)
To-dos:

- [x] Expose the command to defend against cases where that's just super
long
- [x] Tackle the vertical scroll conflict with panel scroll
- [x] Reduce default font-size

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-05 18:50:53 +00:00
Nate Butler
e64f5ff358 agent: Load usage eagerly (#29937)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-05 14:42:21 -04:00
Marshall Bowers
181cd6294f collab: Pass down staff usage in UpdatePlan message (#29939)
This PR fixes an issue where we weren't correctly passing down usage
information in the `UpdatePlan` message for Zed staff.

Release Notes:

- N/A
2025-05-05 18:02:54 +00:00
tidely
769ec59162 ollama: Add tool call support (#29563)
The goal of this PR is to support tool calls using ollama. A lot of the
serialization work was done in
https://github.com/zed-industries/zed/pull/15803 however the abstraction
over language models always disables tools.

## Changelog:

- Use `serde_json::Value` inside `OllamaFunctionCall` just as it's used
in `OllamaFunctionCall`. This fixes deserialization of ollama tool
calls.
- Added deserialization tests using json from official ollama api docs.
- Fetch model capabilities during model enumeration from ollama provider
- Added `supports_tools` setting to manually configure if a model
supports tools

## TODO:

- [x] Fix tool call serialization/deserialization
- [x] Fetch model capabilities from ollama api
- [x] Add tests for parsing model capabilities 
- [ ] Documentation for `supports_tools` field for ollama language model
config
- [ ] Convert between generic language model types
- [x] Pass tools to ollama

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-05-05 17:52:23 +00:00
Nathan Sobo
e9616259d0 Rename Manual profile to Minimal (#29852)
Completely subjective, but I just like it better.

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-05 11:48:26 -06:00
Michael Sloan
7164124512 agent panel: Bring back search within text threads (#29934)
Release Notes:

- N/A
2025-05-05 16:26:43 +00:00
Kirill Bulatov
76c0eded0d Add more documentation about ways to configure language servers and rust-analyzer (#29932)
Release Notes:

- N/A
2025-05-05 16:10:10 +00:00
AidanV
c56a1cf2b1 vim: Fix r enter indentation (#29838)
Release Notes:

- `r enter` now maintains indentation, matching vim

Useful info for this implementation can be found here:

c3f48e3a76/src/normal.c (L4865)
2025-05-05 16:57:32 +01:00
Richard Feldman
4b9b908233 Delete obsolete find_replace_tool description (#29928)
The tool has been deleted, but the description remained.

Release Notes:

- N/A
2025-05-05 11:56:13 -04:00
Marshall Bowers
10bdf39497 collab: Pass down billing information in UpdatePlan message (#29929)
This PR updates the `UpdatePlan` message to include some additional
information about the user's billing subscription usage.

Release Notes:

- N/A
2025-05-05 11:48:31 -04:00
Smit Barmase
07b4480396 editor: Handle more completions sort cases in Rust and Python (#29926)
Closes #29725

Adds 3 more tests for Rust `into` and `await` cases, and Python
`__init__` case. Tweaks sort logic to accommodate them.

Release Notes:

- Improved code completion sort order, handling more cases with Rust and
Python.
2025-05-05 20:48:52 +05:30
Antonio Scandurra
b0414df921 Simplify setting font size for EditToolCard (#29925)
Release Notes:

- N/A
2025-05-05 15:04:00 +00:00
Bennet Bo Fenner
0246ec2dab agent: Tweak MCP server configuration dialog (#29878)
Tweaks the MCP configuration dialog a bit:
- Increase width of popover
- Disable soft 
- Clear errors when hitting confirm

Release Notes:

- N/A
2025-05-05 16:31:04 +02:00
Nate Butler
a72ade8762 Show prompt usage in agent overflow menu (#29922)
This PR adds prompt usage information, and easy access to managing your
account, to the agent overflow menu:

![CleanShot 2025-05-05 at 10 04
20@2x](https://github.com/user-attachments/assets/337a1a0b-6f71-49a0-9fe7-4fbf2ec1fc27)

Currently this UI will only show after making a request. We'll work on
eagerly getting the usage info later.

Release Notes:

- Added current prompt usage information to the agent menu (`...`) for
Zed AI users
2025-05-05 14:22:36 +00:00
Dan Bornstein
1c44cabaea bash: Fix bracket autoclose behavior (#29817)
Add `autoclose_before` configuration for Bash.

Closes #23627

Release Notes:

- Bash: Improved bracket autoclose behavior.
2025-05-05 10:02:27 -04:00
Antonio Scandurra
5674b5cd4d Don't show deleted hunks when agent overwrites file (#29918)
Release Notes:

- Improved display of diffs when the agent rewrites a file from scratch.
2025-05-05 13:13:36 +00:00
Smit Barmase
4a7b3aa4b8 zed: Fix migration message sometimes showing up on other tabs (#29917)
<img width="1178" alt="image"
src="https://github.com/user-attachments/assets/6b76fe7d-0621-4d61-936e-bfe4f72cc614"
/>


Release Notes:

- Fixed an issue where the keymap/settings migration message sometimes
showing up on tabs other than `settings.json` and `keymap.json`.
2025-05-05 18:13:26 +05:30
Cole Miller
c765da1c82 lsp: Don't log oneshot channel errors from notify (#29857)
This is kind of noisy and not very informative.

Release Notes:

- N/A
2025-05-05 08:21:45 -04:00
Cole Miller
b404024c7a Get terminal tool working in evals (#29831)
Bypass our terminal subsystem and just run a shell in a pty.

- [x] make sure we use the same working directory
- [x] strip control chars from the pty output (?)
- [x] tests

Release Notes:

- N/A
2025-05-05 08:07:43 -04:00
Ben Brandt
ce053c9bff Stop generating in the Agent panel when the user edits a previous message (#29915)
Otherwise the panel keeps scrolling as the new token comes in and it is
almost impossible to keep the scroll position in the right place.

Also, if the user is editing, it is likely that the current generated
tokens will need to be regenerated anyway, so we may as well stop the
current progress.

Release Notes:

- Agent Beta: Stop generating tokens if previous messages are edited.
2025-05-05 14:06:02 +02:00
Umesh Yadav
251f26d48a copilot: Add support for tool_calls for gpt-4.1, gpt-4o, o4-mini (#29369)
Github Copilot currently supports following models for agent mode with
tool calls. Currently we are only supporting anthropic models and not
openai and gemini. This PR add support for the openai models. I have
tested it and it works for all of them. For gemini models it seems there
is a issues from copilot side so not adding that in this PR as enabling
gemini model breaks it in the ask mode as well.

<img width="392" alt="image"
src="https://github.com/user-attachments/assets/fb7a4148-e48c-45c5-9ff9-c02f71217dfb"
/>


- [x] GPT-4.1

- [x] GPT-4.0

- [x] o4-mini

Release Notes:

- agent: Add tool calling support for gpt-4.1, gpt-4o, o4-mini when
using Copilot Chat as a provider

Signed-off-by: Umesh Yadav <umesh4257@gmail.com>
2025-05-05 13:59:12 +02:00
Kirill Bulatov
7133699335 Suggest nim extension for *.nim files (#29912)
Release Notes:

- N/A
2025-05-05 11:46:42 +00:00
Antonio Scandurra
1adb4ecc95 Polish diff for the edit_file tool (#29911)
I added some padding to the editor, and removed the border around each
hunk as it would overlap in weird ways with the card container.

## Before

<img width="1148" alt="image"
src="https://github.com/user-attachments/assets/2018feaa-c847-4609-bc82-522660714b9a"
/>

## After

One Light:

<img width="1148" alt="image"
src="https://github.com/user-attachments/assets/4da1a4b6-0af2-4479-afcc-02da50178fd6"
/>

One Dark:

<img width="1148" alt="image"
src="https://github.com/user-attachments/assets/0168631d-7b76-4582-8174-c6e9c1297dc8"
/>


Release Notes:

- Improved displaying of diffs when the agent edits files.
2025-05-05 11:17:15 +00:00
Kirill Bulatov
0048e67832 Properly restore window position for SSH projects (#29904)
Release Notes:

- Fixed SSH projects not restoring their window position on reopen
2025-05-05 08:46:49 +00:00
Finn Evers
0119b66426 project_search: Ensure filter row aligns with other search rows (#29886)
Closes #29858 

This PR fixes the alignment-issue for the project saerch for cases where
the horizontally available space is large.

The issue arose because the two smaller editors within one line were
allowed to grow as much as the other editors on separate lines, up to
1200 pixels. However, these two editors should together only take up
1200 pixels at maximum, including the gap between them. To fix this, the
editors now live within one container element that grows at the same
rate as the other editors whilst allowing both editors to flex grow as
needed in the available space.

Current main:


https://github.com/user-attachments/assets/622016dc-70e5-455f-a7ba-5b69405d7e1e

This PR: 


https://github.com/user-attachments/assets/5244abf7-f0c0-4781-acb7-b774638d8a17

Release Notes:

- Improved project search input field alignment.
2025-05-05 09:35:48 +03:00
Marshall Bowers
45fe158bc9 collab: Improve GET /billing/usage endpoint (#29898)
This PR improves the `GET /billing/usage` endpoint.

We now return the usage with the default plan limits when there is no
usage record.

Release Notes:

- N/A
2025-05-05 02:31:02 +00:00
Marshall Bowers
55eb0710ed agent: Update callout URLs (#29897)
This PR updates the Agent callout URLs to go to the account page.

Release Notes:

- N/A
2025-05-05 01:44:09 +00:00
Marshall Bowers
3e2abbf53b ui: Make Callout constructors more flexible (#29895)
This PR updates the `Callout` constructors to be more flexible by
accepting `impl Into<SharedString>`s.

Release Notes:

- N/A
2025-05-05 01:18:48 +00:00
Marshall Bowers
a2fa10f35f agent: Remove UsageBanner (#29896)
This PR removes the `UsageBanner` component, as it was no longer used.

Release Notes:

- N/A
2025-05-05 01:18:36 +00:00
Marshall Bowers
3db4744e18 agent: Remove unneeded tracking of request usage (#29894)
This PR removes some unneeded tracking of the model request usage in the
`ActiveThread` and `ThreadEvent::UsageUpdated` events.

Release Notes:

- N/A
2025-05-05 01:16:53 +00:00
Nate Butler
fe177f5d69 agent: Add UI for upsell scenarios (#29805)
Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-05 00:48:06 +00:00
Danilo Leal
a19687a815 agent: Sort profiles based on relevance (#29893)
Kinda feel like the way that makes the most sense to sort profiles in
the dropdown is by relevance/impact. "Write" is the default profile and
contains all built-in tools turned on by default, thus it should be the
first. "Ask" contains read-only tools, one step down from Write. And
"Manual" is totally empty, the least "powerful" profile, thus the last.

Release Notes:

- N/A
2025-05-05 00:35:52 +00:00
Nathan Sobo
eb15ed7d60 In the edit tool card, use the UI font size for the editor that we use to render the diff (#29882)
I am currently setting the font size corrrectly by using a custom
EditorStyle and building an element. However I need to use the same
properties as a normal editor for everything but font size.

Release Notes:

- N/A
2025-05-05 00:09:47 +00:00
Danilo Leal
52da375a9d agent: Add design adjustments to message editor (#29891)
- Removed unused `MessageBubbleDashed` icon
- Polished `Crosshair` icon SVG
- Added dropdown toggle keybinding to the profile selector tooltip
- Repositioned buttons at the message editor footer
- Updated buttons to use `Button` instead of `ButtonLike`

Release Notes:

- N/A
2025-05-04 20:21:30 -03:00
Marshall Bowers
3594a52bee collab: Don't try to sync usage to Stripe for staff users (#29892)
This PR makes it so we don't try to sync billing usage to Stripe for
staff users.

Release Notes:

- N/A
2025-05-04 23:14:24 +00:00
Michael Sloan
76ad1a29a5 Add support for getting the token count for all parts of Gemini generation requests (#29630)
* `CountTokensRequest` now takes a full `GenerateContentRequest` instead
of just content.

* Fixes use of `models/` prefix in `model` field of
`GenerateContentRequest`, since that's required for use in
`CountTokensRequest`. This didn't cause issues before because it was
always cleared and used in the path.

Release Notes:

- N/A
2025-05-04 21:32:45 +00:00
Michael Sloan
86484233c0 Replace std::sync::Mutex with parking_lot::Mutex in languages/src/python.rs (#29889)
This appears to be the only place `std::sync::Mutex` is used, Zed always
prefers `parking_lot`.

Release Notes:

- N/A
2025-05-04 21:12:21 +00:00
Michael Sloan
f4e9ea3cd8 In error text of cloud LLM API: completion failed -> request failed (#29888)
This error is used for more requests than completion requests

Release Notes:

- N/A
2025-05-04 21:04:34 +00:00
Marshall Bowers
161f6dfcb6 collab: Set billing-related fields for Zed staff (#29887)
This PR sets the billing-related fields in the LLM token claims for Zed
staff.

Staff members are automatically in the Zed Pro plan with a subscription
periods that spans the entirety of each month.

Release Notes:

- N/A
2025-05-04 21:00:34 +00:00
Michael Sloan
a0895a6ed8 Only send Stop event at end of google completion request (#29885)
I don't think this makes much of a difference in current use, but this
more closely matches other providers and cleans up the "Response"
section of eval markdown output

Release Notes:

- N/A
2025-05-04 20:23:13 +00:00
Michael Sloan
bb82d9ca82 agent eval: Fix --model arg and add --provider (#29883)
Release Notes:

- N/A
2025-05-04 13:43:57 -06:00
ZaraPhu
007685f6d4 docs: Add instructions for uninstalling Zed (#29840) 2025-05-04 17:41:36 +00:00
Max Brunsfeld
c3d9cdecab Change cloud language model provider JSON protocol to surface errors and usage information (#29830)
Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-04 17:37:42 +00:00
Bennet Bo Fenner
3984531a45 agent: Rename @rules to @rule (#29881)
This is purely a cosmetic change, renamed `@rules` to `@rule` which
unifies the @mention experience (for files, threads etc. we also use
`@file`, `@thread` not `@files`, `@thread`). Would also make sense to
rename the rules picker to rule picker, but i do not wanna introduce
conflicts just for the purpose of re-naming.

Release Notes:

- N/A
2025-05-04 16:25:44 +00:00
Marshall Bowers
cceb13b7cd collab: Add use_llm_request_queue to LlmTokenClaims (#29877)
This PR adds a `use_llm_request_queue` field to the LLM token claims,
based on the `llm-request-queue` feature flag.

Release Notes:

- N/A
2025-05-04 12:08:43 -04:00
Marshall Bowers
427101b634 collab: Drop legacy subscription usage and meter tables (#29876)
This PR adds a migration to drop the `subscription_usages` and
`subscription_usage_meters` tables from the database.

We're now using `subscription_usages_v2` and
`subscription_usage_meters_v2` everywhere.

Release Notes:

- N/A
2025-05-04 10:42:40 -04:00
Antonio Scandurra
4d51602e7b Encourage editing over re-creating a file from scratch (#29870)
I also introduced a new eval to prove the encouragement actually makes a
difference.

Release Notes:

- Improved agent behavior when streaming edits, encouraging it to
editing files as opposed to creating them from scratch
2025-05-04 13:18:28 +00:00
Marshall Bowers
ca1dc821cf collab: Fix subscription_usage_id column type (#29871)
This PR fixes the type of the `subscription_usage_id` column on the
`SubscriptionUsageMeter` model.

Release Notes:

- N/A
2025-05-04 13:05:26 +00:00
Danilo Leal
2e3baef299 agent: Polish single-file review toolbar controls (#29866) 2025-05-04 07:53:21 -03:00
Antonio Scandurra
545ae27079 Add the ability to follow the agent as it makes edits (#29839)
Nathan here: I also tacked on a bunch of UI refinement.

Release Notes:

- Introduced the ability to follow the agent around as it reads and
edits files.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-05-04 08:28:39 +00:00
Danilo Leal
425f32e068 agent: Add the single_file_review setting to the UI (#29859)
Release Notes:

- agent: Add the `single_file_review` setting to the UI
2025-05-03 21:01:44 -03:00
Agus Zubiaga
9c11d24887 Fix hiding editor toolbar and add agent_review setting (#29854)
Closes #29836

The agent diff toolbar item was causing the editor toolbar to show even
when all the other elements were disabled via settings.

This PR fixes this by setting the location to
`ToolbarItemLocation::Hidden` in the states where it shouldn't show.

It also adds a new a `toolbar.agent_review` setting to hide the agent
review buttons altogether. However, if the other toolbar elements are
hidden and the file isn't under review, the editor toolbar will still be
hidden. So you only need to set this to `false` if you don't want them
to show up even under agent review.

Release Notes:

- N/A
2025-05-03 17:43:46 -03:00
Marshall Bowers
1fc57ea9f5 feature_flags: Add a constant to control Agent-related feature flags (#29853)
This PR adds a singular constant that controls the Agent-related feature
flags.

This way we can tweak this one value when we're ready to build the final
build for the launch.

Release Notes:

- N/A
2025-05-03 20:16:25 +00:00
Marshall Bowers
c3d2831d86 collab: Use new subscription usage tables (#29848)
This PR updates Collab to use the new subscription usage tables added in
#29847.

Release Notes:

- N/A
2025-05-03 17:56:43 +00:00
Marshall Bowers
c1247977ed collab: Add new tables for subscription usages and meters (#29847)
This PR adds two new tables:

- `subscription_usages_v2`
- `subscription_usage_meters_v2`

These are the same as the old ones, except using UUIDs as primary keys.

Release Notes:

- N/A
2025-05-03 17:21:22 +00:00
Marshall Bowers
12c26a4fa6 collab: Don't try to transfer usage when a Zed Pro trial is canceled (#29843)
This PR fixes an issue where we would erroneously try to transfer
existing subscription usage when a Zed Pro trial was canceled.

Release Notes:

- N/A
2025-05-03 14:57:54 +00:00
Marshall Bowers
7f8e3fd482 ui: Implement ParentElement for Banner (#29834)
This PR implements the `ParentElement` trait for the `Banner` component
so that it can use the real children APIs instead of a bespoke one.

Release Notes:

- N/A
2025-05-03 02:36:53 +00:00
Marshall Bowers
f0515d1c34 agent: Show a notice when reaching consecutive tool use limits (#29833)
This PR adds a notice when reaching consecutive tool use limits when
using normal mode.

Here's an example with the limit artificially lowered to 2 consecutive
tool uses:


https://github.com/user-attachments/assets/32da8d38-67de-4d6b-8f24-754d2518e5d4

Release Notes:

- agent: Added a notice when reaching consecutive tool use limits when
using a model in normal mode.
2025-05-03 02:09:54 +00:00
Danilo Leal
10a7f2a972 agent: Add several UX improvements (#29828)
Still a work in progress.

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
2025-05-02 19:00:55 -06:00
Danilo Leal
5053562e28 agent: Refresh the profile selector and modal design (#29816)
- [x] Separate MCP servers from tools in the profile customization modal
view
- [x] Group MCP tools in the MCP picker and add a heading
- [x] Separate bult-in profiles from custom ones in the dropdown
selector
- [x] Separate bult-in profiles from custom ones in the modal
- [ ] Enable looping through items via keybinding without opening the
dropdown (will be done on a follow-up PR)
- [ ] Stretch: Focus on the currently active item upon opening the
dropdown (will be done on a follow-up PR)

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
2025-05-02 20:34:36 -03:00
Agus Zubiaga
1877fce609 agent: Fix default cursor position on reviewing editors (#29825)
The cursor wasn't always placed at the first hunk for review editors.

Release Notes:

- N/A
2025-05-02 21:58:00 +00:00
Agus Zubiaga
64316309aa agent: Review edits in single-file editors (#29820)
Enables reviewing agent edits from single-file editors in addition to
the multibuffer experience we already had.


https://github.com/user-attachments/assets/a2c287f0-51d6-43a1-8537-821498b91983


This feature can be turned off by setting `assistant.single_file_review:
false`.

Release Notes:

- agent: Review edits in single-file editors
2025-05-02 17:57:16 -03:00
Max Brunsfeld
04772bf17d Add support for queuing status updates in cloud language model provider (#29818)
This sets us up to display queue position information to the user, once
our language model backend is updated to support request queuing.

The JSON returned by the LLM backend will need to look like this:

```json
{"queue": {"status": "queued", "position": 1}}
{"queue": {"status": "started"}}
{"event": {"THE_UPSTREAM_MODEL_PROVIDER_EVENT": "..."}} 
```

Release Notes:

- N/A

---------

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-05-02 20:36:39 +00:00
Richard Feldman
4d1df7bcd7 Re-enable directory-related tools (#29809)
Also `now` in `write` profile

Release Notes:

- Tools for manipulating directories no longer require confirmation, and
are enabled in the Write profile
- Enabled `now` and `list_directory` tools by default in Write profile

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-02 16:11:16 -04:00
Cole Miller
9547d42b15 Support @-mentions in inline assists and when editing old agent panel messages (#29734)
Closes #ISSUE

Co-authored-by: Bennet <bennet@zed.dev>

Release Notes:

- Added support for context `@mentions` in the inline prompt editor and
when editing past messages in the agent panel.

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-05-02 20:08:53 +00:00
Umesh Yadav
c918f6cde1 agent: Add assistant panel width persistence (#28808)
Previously, the assistant panel width was not persisted across sessions.
This meant that upon restarting the Zed editor, the panel would revert
to its default size, disrupting the user's preferred layout.

This pull request introduces persistence for the assistant panel width.
The width is now saved to the key-value store when the editor is closed
and restored on startup, ensuring a consistent UI experience across
different sessions.

Release Notes:

- agent: Add assistant panel width persistence

---------

Signed-off-by: Umesh Yadav <umesh4257@gmail.com>
2025-05-02 13:05:03 -07:00
Anthony Eid
da98e300cc debugger: Clear active debug line on thread continued (#29811)
I also moved the breakpoint store to session from local mode, because
both remote/local modes will need the ability to remove active debug
lines.

Release Notes:

- N/A
2025-05-02 15:24:28 -04:00
Richard Feldman
e6b0d8e48b Delete obsolete tools (#29808)
Release Notes:

- Removed some obsolete tools: batch_tool, code_actions, code_symbols,
contents, symbol_info, rename

Co-authored-by: Cole Miller <m@cole-miller.net>
2025-05-02 18:52:42 +00:00
Bennet Bo Fenner
9147f89257 zed_extension_api: Release v0.5.0 (#29802)
This PR releases v0.5.0 of the Zed extension API.

Support for this version of the extension API will land in Zed v0.186.x.

Release Notes:

- N/A
2025-05-02 15:58:54 +00:00
Richard Feldman
9efc09c5a6 Add eval for open_tool (#29801)
Also have its description say it should only be used on request

Release Notes:

- N/A
2025-05-02 15:56:07 +00:00
Bennet Bo Fenner
e6f6b351b7 extension_api: Add documentation to context server configuration (#29800)
Release Notes:

- N/A
2025-05-02 15:37:05 +00:00
Bennet Bo Fenner
fde621f0e3 agent: Ensure that web search tool is always available (#29799)
Some changes in the LanguageModelRegistry caused the web search tool not
to show up, because the `DefaultModelChanged` event is not emitted at
startup anymore.

Release Notes:

- agent: Fixed an issue where the web search tool would not be available
after starting Zed (only when using zed.dev as a provider).
2025-05-02 15:34:08 +00:00
Marshall Bowers
c4556e9909 collab: Fix adding users to feature flags when migrating to new billing (#29795)
This PR fixes an issue where users were not being added to the feature
flags when being migrated to the new billing.

Release Notes:

- N/A
2025-05-02 15:07:49 +00:00
Kirill Bulatov
7e2de84155 Properly score fuzzy match queries with multiple chars in lower case (#29794)
Closes https://github.com/zed-industries/zed/issues/29526

Release Notes:

- Fixed file finder crashing for certain file names with multiple chars
in lowercase form
2025-05-02 15:02:53 +00:00
Kirill Bulatov
d1b35be353 Use proper settings in the diagnostics section (#29791)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Release Notes:

- N/A

Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-02 16:48:52 +03:00
Marshall Bowers
49a71ec3b8 collab: Update billing migration endpoint to work for users without active subscriptions (#29792)
This PR updates the billing migration endpoint to work for users who do
not have an active subscription.

This will allow us to use the endpoint to migrate all users.

Release Notes:

- N/A
2025-05-02 13:48:14 +00:00
Nate Butler
3bd7ae6e5b Standardize agent previews (#29790)
This PR makes agent previews render like any other preview in the
component preview list & pages.

Page:

![CleanShot 2025-05-02 at 09 17
12@2x](https://github.com/user-attachments/assets/8b611380-b686-4fd6-9c76-de27e35b0b38)

List:

![CleanShot 2025-05-02 at 09 17
33@2x](https://github.com/user-attachments/assets/ab063649-dc3c-4c95-969b-c3795b2197f2)


Release Notes:

- N/A
2025-05-02 13:32:59 +00:00
Max Brunsfeld
225deb6785 agent: Add animation in the edit file tool card until a diff is assigned (#29773)
This PR prevents this edit card from being shown expanded but empty,
like this:

<img width="590" alt="Screenshot 2025-05-01 at 7 38 47 PM"
src="https://github.com/user-attachments/assets/147d3d73-05b9-4493-8145-0ee915f12cd9"
/>

Now, we will show an animation until it has a diff computed.


https://github.com/user-attachments/assets/52900cdf-ee3d-4c3b-88c7-c53377543bcf

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-05-02 09:48:40 -03:00
Kirill Bulatov
33011f2eaf Open diagnostics editor faster when fetching cargo diagnostics (#29787)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Release Notes:

- N/A
2025-05-02 12:10:01 +00:00
Kirill Bulatov
e14d078f8a Fix tasks not being stopped on reruns (#29786)
Follow-up of https://github.com/zed-industries/zed/pull/28993

* Tone down tasks' cancellation logging
* Fix task terminals' leak, disallowing to fully cancel the task by
dropping the terminal off the pane:

f619d5f02a/crates/terminal_view/src/terminal_panel.rs (L1464-L1471)

Release Notes:

- Fixed tasks not being stopped on reruns
2025-05-02 11:45:43 +00:00
Stanislav Alekseev
460ac96df4 Use project environment in LSP runnables context (#29761)
Release Notes:

- Fixed the tasks from LSP not inheriting the worktree environment

----

cc @SomeoneToIgnore
2025-05-02 11:01:39 +00:00
Antonio Scandurra
35539847a4 Allow StreamingEditFileTool to also create files (#29785)
Refs #29733 

This pull request introduces a new field to the `StreamingEditFileTool`
that lets the model create or overwrite a file in a streaming way. When
one of the `assistant.stream_edits` setting / `agent-stream-edits`
feature flag is enabled, we are going to disable the `CreateFileTool` so
that the agent model can only use `StreamingEditFileTool` for file
creation.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-05-02 09:57:04 +00:00
Anthony Eid
f619d5f02a debugger: Add debug task picker to new session modal (#29702)
## Preview 

![image](https://github.com/user-attachments/assets/203a577f-3b38-4017-9571-de1234415162)


### TODO
- [x] Add scenario picker to new session modal
- [x] Make debugger start action open new session modal instead of task
modal
- [x] Fix `esc` not clearing the cancelling the new session modal while
it's in scenario or attach mode
- [x] Resolve debug scenario's correctly

Release Notes:

- N/A
2025-05-02 08:38:29 +00:00
Kirill Bulatov
ba59305510 Use rust-analyzer's flycheck as source of cargo diagnostics (#29779)
Follow-up of https://github.com/zed-industries/zed/pull/29706

Instead of doing `cargo check` manually, use rust-analyzer's flycheck:
at the cost of more sophisticated check command configuration, we keep
much less code in Zed, and get a proper progress report.

User-facing UI does not change except `diagnostics_fetch_command` and
`env` settings removed from the diagnostics settings.

Release Notes:

- N/A
2025-05-02 10:07:51 +03:00
Nate Butler
672a1dd553 Add Agent Preview trait (#29760)
Like the title says

Release Notes:

- N/A
2025-05-01 23:03:06 -04:00
Marshall Bowers
93cc4946d8 agent: Make thread completion mode non-optional (#29772)
This PR makes the thread completion mode non-optional.

Release Notes:

- N/A
2025-05-02 02:41:54 +00:00
Marshall Bowers
0c0a4ed866 collab: Return increased limit for extended trials from GET /billing/usage (#29771)
This PR updates the `GET /billing/usage` endpoint to return the
increased usage limit for users in the extended trial.

Release Notes:

- N/A
2025-05-02 02:31:30 +00:00
Marshall Bowers
51f1998107 Fix typo in typos.toml (#29770)
This PR fixes a typo in `typos.toml`. How ironic.

Release Notes:

- N/A
2025-05-02 02:01:07 +00:00
Marshall Bowers
1ffedf4a08 collab: Add endpoint for migrating users to new billing (#29769)
This PR adds a new `POST /billing/subscriptions/migrate` endpoint for
migrating users to the new billing system.

When called with a GitHub user ID this endpoint will:

1. Find the active billing subscription for this user (if they have one)
2. Cancel the subscription and send a final invoice
3. Ensure the user is in the `new-billing` and `assistant2` feature
flags

Release Notes:

- N/A
2025-05-02 01:47:09 +00:00
Cole Miller
d25da9728b Run additional checks from script/clippy if local (#29768)
Should cut down on the number of CI cycles if you're forgetful like I
am!

Release Notes:

- N/A
2025-05-02 01:26:12 +00:00
Cole Miller
e1e3f2e423 Improve handling of remote-tracking branches in the picker (#29744)
Release Notes:

- Changed the git branch picker to make remote-tracking branches less
prominent

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-05-01 21:24:26 -04:00
Finn Evers
92b9ecd7d2 agent: Do not render unnecessary lines in edit file tool card (#29766)
This PR prevents any unnecessary lines from being rendered in the edit
file tool card in the case of small diffs.

I think this (hopefully) addresses the last remaining task from
https://github.com/zed-industries/zed/pull/29448.

| `main` | This PR |
| --- | --- |
| <img width="634" alt="main"
src="https://github.com/user-attachments/assets/7c06394e-957a-4d36-a484-5974687041e9"
/> | <img width="634" alt="PR"
src="https://github.com/user-attachments/assets/84206d5a-a93a-4a42-99ca-7cdebb0d91bb"
/> |

(The last empty line in the second image is an empty line present in the
file itself)

---

n the second commit I also preemtively disabled vertical overscrolling
for full mode editors which are sized by content. This is basically the
same fix as in https://github.com/zed-industries/zed/pull/28471.
Strictly speaking, this is not needed for the fix here, but I thought it
might be nice to have for the future to prevent any issues from occuring
due to overscroll.

Release Notes:

- agent: Improved rendering of small diffs for the edit file tool card.
2025-05-01 20:40:12 -03:00
Marshall Bowers
758d260cec collab: Add ability to initiate a checkout session for the Zed Free plan (#29767)
This PR adds the ability to initiate a checkout session for the Zed Free
plan.

Release Notes:

- N/A
2025-05-01 23:35:23 +00:00
Danilo Leal
8d4d3badf3 agent: Add design adjustments to MCP config flow (#29765)
Mostly somewhat small UI tweaks around the MCP extension config flow and
the settings section.

Release Notes:

- N/A
2025-05-01 19:29:59 -03:00
Marshall Bowers
7c23d13773 agent: Render the max mode toggle using a muted color (#29763)
This PR updates the max mode toggle to use the muted color.

This makes it fit in more with the rest of the controls.

<img width="243" alt="Screenshot 2025-05-01 at 5 24 01 PM"
src="https://github.com/user-attachments/assets/57267d29-3c7b-4ea9-b6b9-81c42f6b7e1c"
/>

Release Notes:

- agent: Adjusted the color of the max mode toggle.
2025-05-01 21:40:10 +00:00
Richard Feldman
ad87c545c7 Make context pills clickable while editing (#29740)
Release Notes:

- Fixed a bug where clicking context pills switched into the "editing
message" state instead of clicking the pill.

Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Ben <ben@zed.dev>
2025-05-01 20:28:54 +00:00
Richard Feldman
23fbab15ee Manual no tool calls (#29745)
Now instead of the model hallucinating tool calls, we get requests for
more context:

<img width="620" alt="Screenshot 2025-05-01 at 12 45 49 PM"
src="https://github.com/user-attachments/assets/847d5c14-82f6-4234-b85a-8cd2bc7ab11d"
/>

It still knows how to answer general questions:
<img width="624" alt="Screenshot 2025-05-01 at 12 47 44 PM"
src="https://github.com/user-attachments/assets/43ab0fc3-4cc8-452f-b26b-474b5d31919f"
/>

Release Notes:

- Fixed the model still trying to do tool calls when no tools selected
(e.g. in `Manual` profile).

---------

Co-authored-by: Ben <ben@zed.dev>
Co-authored-by: Michael <michael@zed.dev>
2025-05-01 16:11:13 -04:00
Richard Feldman
d7e181576e Respect cursor_pointer when a ButtonLike is disabled (#29737)
This is desirable for when we want to use a `ButtonLike` to show a
tooltip over an icon, and we don't want it to show the "not allowed"
cursor on hover.

Release Notes:

- N/A
2025-05-01 15:34:40 -04:00
Eva Pace
9788aff4b1 Fix license symlinks (#29758)
Closes #29527

It looks funny in the diff, but the symlinks are indeed correct:

-
https://github.com/evaporei/zed/blob/fix/license-symlinks/crates/askpass/LICENSE-GPL
-
https://github.com/evaporei/zed/blob/fix/license-symlinks/crates/ui_macros/LICENSE-GPL

I did check all ~170 crates, these were the only inconsistent ones.

Release Notes:

- N/A
2025-05-01 19:24:14 +00:00
Kirill Bulatov
2a319efade Add editor::GoToParentModule for rust-analyzer backed projects (#29755)
Support rust-analyzer's "go to parent module" action


https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#parent-module

Release Notes:

- Added `editor::GoToParentModule` for rust-analyzer backed projects

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-05-01 18:28:05 +00:00
Jonathan LEI
50ec26c163 Fix user rules ignored by agent (#29754)
Closes #29753

The template contains an error: `has_default_user_rules` is always
undefined and should be `has_user_rules` instead.

Release Notes:

- Fixed default user rules ignored during prompt building.
2025-05-01 18:22:48 +00:00
Danilo Leal
39dd133b1c agent: Remove unused agent: chat mode command palette action (#29741)
We weren't using this one anymore. We used to use it for the switch that
toggled tools on, which doesn't exist anymore.

Release Notes:

- N/A

---------

Co-authored-by: Joseph T. Lyons <josephtlyons@gmail.com>
2025-05-01 15:09:14 -03:00
Bennet Bo Fenner
24eb039752 context servers: Show configuration modal when extension is installed (#29309)
WIP

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Marshall Bowers <git@maxdeviant.com>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-05-01 20:02:14 +02:00
Peter Tripp
bffa53d706 docs: Reorder macOS development documentation (#29751)
Release Notes:

- N/A
2025-05-01 17:34:17 +00:00
Bennet Bo Fenner
0e5e8f9f8d Allow MIT-0 license in checks (#29748)
Part of #29309

The license is on par with other licenses in the list:
https://github.com/aws/mit-0

Release Notes:

- N/A
2025-05-01 17:30:16 +00:00
Danilo Leal
96d785cb45 git: Improve co-author button (#29742)
This PR changes the tooltip label to say "Remove" when you have the
button toggled on and collaborators in the list.

Release Notes:

- N/A

Co-authored-by: Joseph T. Lyons <josephtlyons@gmail.com>
2025-05-01 14:12:52 -03:00
Marshall Bowers
57610c9935 collab: Add billing thresholds to request overage subscription items (#29738)
This PR adds billing thresholds of the unit equivalent of $20 for model
request overages.

Release Notes:

- N/A
2025-05-01 16:10:06 +00:00
Marshall Bowers
5bf1b4f0a8 collab: Add use_new_billing to LlmTokenClaims (#29739)
This PR adds a `use_new_billing` field to the LLM token claims, based on
the `new-billing` feature flag.

Release Notes:

- N/A
2025-05-01 15:43:53 +00:00
Antonio Scandurra
f891dfb358 Introduce a new StreamingEditFileTool (#29733)
This pull request introduces a new tool for streaming edits. The
short-term goal is for this tool to replace the existing `EditFileTool`,
but we want to get this out the door as soon as possible so that we can
start testing it.

`StreamingEditFileTool` is mutually exclusive with `EditFileTool`. It
will be enabled by default for anyone who has the `agent-stream-edits`
feature flag, as well as people that set `assistant.stream_edits` to
`true` in their settings.

### Implementation

Streaming is achieved by requesting a completion while the `edit_file`
tool gets called. We invoke the model by taking the existing
conversation with the agent and appending a prompt specifically tailored
for editing. In that prompt, we ask the model to produce a stream of
`<old_text>`/`<new_text>` tags. As the model streams text in, we
incrementally parse it and start editing as soon as we can.

### Evals

Note that, as part of this pull request, I also defined some new evals
that I used to drive the behavior of the recursive LLM call. To run
them, use this command:

```bash
cargo test --package=assistant_tools --features eval -- eval_extract_handle_command_output
```

Or comment out the `#[cfg_attr(not(feature = "eval"), ignore)]` macro.

I recommend running them one at a time, because right now we don't
really have a way of orchestrating of all these evals. I think we should
invest into that effort once the new agent panel goes live.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com>
2025-05-01 17:37:43 +02:00
Ben Kunkle
e3a2d52472 zlog: Fall back to printing module path instead of *unknown* or just crate name (#29691)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-05-01 10:59:51 -04:00
Danilo Leal
122af4fd53 agent: Show nav dropdown close button only on hover (#29732) 2025-05-01 11:21:57 -03:00
Kirill Bulatov
e07ffe7cf1 Allow to fetch cargo diagnostics separately (#29706)
Adjusts the way `cargo` and `rust-analyzer` diagnostics are fetched into
Zed.

Nothing is changed for defaults: in this mode, Zed does nothing but
reports file updates, which trigger rust-analyzers'
mechanisms:

* generating internal diagnostics, which it is able to produce on the
fly, without blocking cargo lock.
Unfortunately, there are not that many diagnostics in r-a, and some of
them have false-positives compared to rustc ones

* running `cargo check --workspace --all-targets` on each file save,
taking the cargo lock
For large projects like Zed, this might take a while, reducing the
ability to choose how to work with the project: e.g. it's impossible to
save multiple times without long diagnostics refreshes (may happen
automatically on e.g. focus loss), save the project and run it instantly
without waiting for cargo check to finish, etc.

In addition, it's relatively tricky to reconfigure r-a to run a
different command, with different arguments and maybe different env
vars: that would require a language server restart (and a large project
reindex) and fiddling with multiple JSON fields.

The new mode aims to separate out cargo diagnostics into its own loop so
that all Zed diagnostics features are supported still.


For that, an extra mode was introduced:

```jsonc
"rust": {
  // When enabled, Zed runs `cargo check --message-format=json`-based commands and
  // collect cargo diagnostics instead of rust-analyzer.
  "fetch_cargo_diagnostics": false,
  // A command override for fetching the cargo diagnostics.
  // First argument is the command, followed by the arguments.
  "diagnostics_fetch_command": [
    "cargo",
    "check",
    "--quiet",
    "--workspace",
    "--message-format=json",
    "--all-targets",
    "--keep-going"
  ],
  // Extra environment variables to pass to the diagnostics fetch command.
  "env": {}
}
```

which calls to cargo, parses its output and mixes in with the existing
diagnostics:




https://github.com/user-attachments/assets/e986f955-b452-4995-8aac-3049683dd22c




Release Notes:

- Added a way to get diagnostics from cargo and rust-analyzer without
mutually locking each other
- Added `ctrl-r` binding to refresh diagnostics in the project
diagnostics editor context
2025-05-01 11:25:52 +03:00
Finn Evers
5e4be013af zed: Fix application menu capitalization (#29722)
This PR is a quick follow-up to #29717 to ensure that the action within
the app menu has the same capitalization as in the context menu.

Release Notes:

- N/A
2025-05-01 08:19:02 +00:00
Aaron Feickert
f055dca592 editor: Fix context menu capitalization (#29717)
Fixes context menu capitalization for consistency.

Release Notes:

- N/A
2025-05-01 09:58:08 +03:00
Richard Feldman
5872276511 Re-enable open tool (#29707)
Release Notes:

- Added `open` tool for opening files or URLs.
2025-04-30 22:33:52 -04:00
Bennet Bo Fenner
1bf9e15f26 agent: Allow adding/removing context when editing existing messages (#29698)
Release Notes:

- agent: Support adding/removing context when editing existing message

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-01 01:39:34 +00:00
Marshall Bowers
f046d70625 collab: Look up Stripe prices with lookup keys (#29715)
This PR makes it so we look up Stripe prices via lookup keys instead of
passing in the price IDs as environment variables.

Release Notes:

- N/A
2025-05-01 00:26:31 +00:00
Richard Feldman
afeb3d4fd9 Make eval more resilient to bad input from LLM (#29703)
I saw a slice panic (for begin > end) in a debug build of the eval. This
should just be a failed assertion, not a panic that takes out the whole
eval run!

Release Notes:

- N/A
2025-04-30 18:13:45 -04:00
Richard Feldman
92dd6b67c7 Fix potential subtraction overflow (#29697)
I saw this come up in an eval, where the LLM provided a start line of 0.

Release Notes:

- N/A
2025-04-30 18:13:37 -04:00
Cole Miller
38ede4bae3 Fix parsing of author name in git show output (#29704)
Closes #ISSUE

Release Notes:

- Fixed a bug causing incorrect formatting of git commit tooltips
2025-04-30 20:54:53 +00:00
Ben Kunkle
fc920bf63d Improve behavior around word-based brackets in bash (#29700)
Closes #28414

Makes it so that `do`, `then`, `done`, `else`, etc are treated as
brackets in bash. They are not auto-closed *yet* as that requires
additional work to function properly, however they can now be toggled
between using `%` in vim. Additionally, newlines are inserted like they
are with regular brackets (`{}()[]""''`) when hitting enter between
them.

While `if <-> fi` `while/for <-> done` and `case <-> esac` are the
*logical* matching pairs, I've opted to instead match between `then <->
else/elif/fi` `do <-> done` and `in <-> esac` as these are the pairs
that delimit the sub-scope, and are more similar to the `{}` style
bracket pairs than `if <-> }` in a c-like syntax. This does cause some
wierd behavior with `else` in `if` expressions as it matches both with
the previous `then` as well as the following `fi`, so in this case

```bash
if true; then
   foo
else
   bar
f|i
```

after hitting `%` twice times (where cursor is `|`), the cursor will end
up on the `then` instead of back on the `fi` as hitting `%` on the else
will *always* navigate up to the `then`

Release Notes:

- vim: Improved behavior around word-based delimiters in bash (`do <->
done`, `then <-> fi`, etc) so they can be toggled between using `%`
2025-04-30 19:57:29 +00:00
Richard Feldman
04c68dc0cf Make the default repetitions be 8, and concurrency 4 (#29576)
This is based on having observed that there is a lot of variation
between runs on `n=1` and `n=3`.

* With `n=8` two runs on the same branch give answers that seem close
enough to be reasonably consistent.
* With higher concurrency, trying to run this many repetitions seems to
lead language servers to time out a lot, causing evals to fail.

Release Notes:

- N/A
2025-04-30 15:21:19 -04:00
Marshall Bowers
399eced884 collab: Return current usage by model from GET /billing/usage (#29693)
This PR updates the `GET /billing/usage` endpoint to return the number
of requests made to each model and mode.

Release Notes:

- N/A
2025-04-30 19:06:39 +00:00
Richard Feldman
50f705e779 Use outline (#29687)
## Before

![Screenshot 2025-04-30 at 10 56
36 AM](https://github.com/user-attachments/assets/3a435f4c-ad45-4f26-a847-2d5c9d03648e)

## After

![Screenshot 2025-04-30 at 10 55
27 AM](https://github.com/user-attachments/assets/cc3a8144-b6fe-4a15-8a47-b2487ce4f66e)

Release Notes:

- Context picker and `@`-mentions now work with very large files.
2025-04-30 18:00:00 +00:00
Ben Kunkle
8173534ad5 zed: Reinstate default file_scan_exclusions in Zed repo project settings (#29690)
Closes #ISSUE

Re-adds default `file_scan_exclusions` to [project
settings](84e4891d54/.zed/settings.json)
that were overridden in #29106

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-04-30 17:50:56 +00:00
Nate Butler
8c03934b26 welcome: Theme preview tile (#29689)
![CleanShot 2025-04-30 at 13 26
44@2x](https://github.com/user-attachments/assets/f68fefe2-84a1-48b7-b9a2-47c2547cd06b)


- Adds the ThemePreviewTile component, used for upcoming onboarding UI
- Adds the CornerSolver utility for resolving correct nested corner
radii

Release Notes:

- N/A
2025-04-30 17:46:11 +00:00
Patrick
84e4891d54 file_finder: Add skip_focus_for_active_in_search setting (#27624)
Closes #27073

Currently, when searching for a file with Ctrl+P, and the first file
found is the active one, file_finder skips focus to the second file
automatically. This PR adds a setting to disable this and make the first
file always the focused one.

Default setting is still skipping the active file.

Release Notes: 

- Added the `skip_focus_for_active_in_search` setting for the file
finder, which allows turning off the default behavior of skipping focus
on the active file while searching in the file finder.

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-04-30 22:58:33 +05:30
Ben Kunkle
d03d8ccec1 python: Fix identification of runnable tests within decorated test classes (#29688)
Closes #29486

Release Notes:

- python: Fixed identification of runnable test functions within
decorated pytest classes
2025-04-30 17:26:30 +00:00
Joseph T. Lyons
4d934f2884 Bump Zed to v0.186 (#29680)
Release Notes:

-N/A
2025-04-30 12:52:25 -04:00
Smit Barmase
e697cf9747 editor: Fix edit range for linked edits on do completion (#29650)
Closes #29544

Fixes an issue where accepting an HTML completion would correctly edit
the start tag but incorrectly update the end tag due to incorrect linked
edit ranges.

I want to handle multi cursor case (as it barely works now), but seems
like this should go first. As, it might need whole `do_completions`
overhaul.

Todo:
- [x] Tests for completion aceept on linked edits

Before:


https://github.com/user-attachments/assets/917f8d2a-4a0f-46e8-a004-675fde55fe3d

After:


https://github.com/user-attachments/assets/84b760b6-a5b9-45c4-85d8-b5dccf97775f

Release Notes:

- Fixes an issue where accepting an HTML completion would correctly edit
the start tag but incorrectly update the end tag.
2025-04-30 21:44:20 +05:30
435 changed files with 16470 additions and 41127 deletions

View File

@@ -162,13 +162,23 @@ jobs:
working-directory: ./docs
run: |
pnpm dlx prettier@${PRETTIER_VERSION} . --check || {
echo "To fix, run from the root of the zed repo:"
echo "To fix, run from the root of the Zed repo:"
echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .."
false
}
env:
PRETTIER_VERSION: 3.5.0
- name: Prettier Check on default.json
run: |
pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --check || {
echo "To fix, run from the root of the Zed repo:"
echo " pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --write"
false
}
env:
PRETTIER_VERSION: 3.5.0
# To support writing comments that they will certainly be revisited.
- name: Check for todo! and FIXME comments
run: script/check-todos

View File

@@ -69,7 +69,7 @@ jobs:
run: cargo build --package=eval
- name: Run eval
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code

3
.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@@ -46,5 +46,17 @@
"formatter": "auto",
"remove_trailing_whitespace_on_save": true,
"ensure_final_newline_on_save": true,
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
"file_scan_exclusions": [
"crates/eval/worktrees/",
"crates/eval/repos/",
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
"**/.classpath",
"**/.settings"
]
}

305
Cargo.lock generated
View File

@@ -56,6 +56,7 @@ dependencies = [
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"async-watch",
"buffer_diff",
@@ -78,6 +79,7 @@ dependencies = [
"heed",
"html_to_markdown",
"http_client",
"indexed_docs",
"indoc",
"itertools 0.14.0",
"jsonschema",
@@ -470,68 +472,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "assistant"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"async-watch",
"client",
"collections",
"command_palette_hooks",
"ctor",
"db",
"editor",
"env_logger 0.11.8",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
"indexed_docs",
"indoc",
"language",
"language_model",
"language_model_selector",
"languages",
"log",
"lsp",
"menu",
"multi_buffer",
"parking_lot",
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"rope",
"rules_library",
"schemars",
"search",
"serde",
"serde_json_lenient",
"settings",
"smol",
"streaming_diff",
"telemetry",
"telemetry_events",
"terminal",
"terminal_view",
"text",
"theme",
"tree-sitter-md",
"ui",
"unindent",
"util",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "assistant_context_editor"
version = "0.1.0"
@@ -573,7 +513,6 @@ dependencies = [
"settings",
"smallvec",
"smol",
"strum 0.27.1",
"telemetry_events",
"text",
"theme",
@@ -737,6 +676,7 @@ dependencies = [
"language_models",
"linkme",
"log",
"markdown",
"open",
"paths",
"portable-pty",
@@ -1330,9 +1270,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
version = "1.13.0"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878"
checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -1340,9 +1280,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.28.2"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
dependencies = [
"bindgen 0.69.5",
"cc",
@@ -2089,7 +2029,7 @@ dependencies = [
[[package]]
name = "blade-graphics"
version = "0.6.0"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
dependencies = [
"ash",
"ash-window",
@@ -2108,6 +2048,7 @@ dependencies = [
"naga",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"objc2-quartz-core",
@@ -2121,7 +2062,7 @@ dependencies = [
[[package]]
name = "blade-macros"
version = "0.3.0"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
dependencies = [
"proc-macro2",
"quote",
@@ -2131,7 +2072,7 @@ dependencies = [
[[package]]
name = "blade-util"
version = "0.2.0"
source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae64602ae74f#b16f5c7bd873c7126f48c82c39e7ae64602ae74f"
source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad"
dependencies = [
"blade-graphics",
"bytemuck",
@@ -2178,9 +2119,9 @@ dependencies = [
[[package]]
name = "block2"
version = "0.5.1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [
"objc2",
]
@@ -3002,7 +2943,6 @@ name = "collab"
version = "0.44.0"
dependencies = [
"anyhow",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
@@ -3238,6 +3178,7 @@ dependencies = [
"gpui",
"linkme",
"parking_lot",
"strum 0.27.1",
"theme",
"workspace-hack",
]
@@ -4067,7 +4008,6 @@ dependencies = [
"http_client",
"language",
"log",
"lsp-types",
"node_runtime",
"parking_lot",
"paths",
@@ -4100,9 +4040,9 @@ dependencies = [
"anyhow",
"async-trait",
"dap",
"futures 0.3.31",
"gpui",
"language",
"lsp-types",
"paths",
"serde",
"serde_json",
@@ -4247,6 +4187,7 @@ dependencies = [
"collections",
"command_palette_hooks",
"dap",
"dap_adapters",
"db",
"debugger_tools",
"editor",
@@ -4259,6 +4200,7 @@ dependencies = [
"log",
"menu",
"parking_lot",
"paths",
"picker",
"pretty_assertions",
"project",
@@ -4393,6 +4335,7 @@ dependencies = [
"ctor",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
"gpui",
"indoc",
"language",
@@ -4515,6 +4458,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"objc2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -6405,6 +6358,7 @@ checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"cfg-if",
"crunchy",
"num-traits",
]
[[package]]
@@ -7794,6 +7748,7 @@ dependencies = [
"tree-sitter-html",
"tree-sitter-json",
"tree-sitter-md",
"tree-sitter-python",
"tree-sitter-ruby",
"tree-sitter-rust",
"tree-sitter-typescript",
@@ -7901,6 +7856,7 @@ dependencies = [
"partial-json-fixer",
"project",
"proto",
"release_channel",
"schemars",
"serde",
"serde_json",
@@ -7981,6 +7937,7 @@ dependencies = [
"log",
"lsp",
"node_runtime",
"parking_lot",
"paths",
"pet",
"pet-conda",
@@ -8572,6 +8529,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assets",
"base64 0.22.1",
"env_logger 0.11.8",
"gpui",
"language",
@@ -8960,23 +8918,27 @@ checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "naga"
version = "23.1.0"
version = "25.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
"bit-set 0.8.0",
"bitflags 2.9.0",
"cfg_aliases 0.1.1",
"codespan-reporting 0.11.1",
"cfg_aliases 0.2.1",
"codespan-reporting 0.12.0",
"half",
"hashbrown 0.15.2",
"hexf-parse",
"indexmap",
"log",
"num-traits",
"once_cell",
"rustc-hash 1.1.0",
"spirv",
"termcolor",
"thiserror 1.0.69",
"unicode-xid",
"strum 0.26.3",
"thiserror 2.0.12",
"unicode-ident",
]
[[package]]
@@ -9430,96 +9392,37 @@ dependencies = [
"objc_id",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
[[package]]
name = "objc2"
version = "0.5.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
dependencies = [
"objc-sys",
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
"objc2-core-data",
"objc2-core-image",
"objc2-core-foundation",
"objc2-foundation",
"objc2-quartz-core",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
name = "objc2-core-foundation"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.0",
"block2",
"dispatch2",
"objc2",
"objc2-core-location",
"objc2-foundation",
]
[[package]]
name = "objc2-contacts"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-data"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-core-image"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
"objc2-metal",
]
[[package]]
name = "objc2-core-location"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2",
"objc2",
"objc2-contacts",
"objc2-foundation",
]
[[package]]
@@ -9530,106 +9433,53 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]]
name = "objc2-foundation"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.9.0",
"block2",
"libc",
"objc2",
]
[[package]]
name = "objc2-link-presentation"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"objc2-core-foundation",
]
[[package]]
name = "objc2-metal"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874"
dependencies = [
"bitflags 2.9.0",
"block2",
"dispatch2",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
]
[[package]]
name = "objc2-symbols"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-ui-kit"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-image",
"objc2-core-location",
"objc2-core-foundation",
"objc2-foundation",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-symbols",
"objc2-uniform-type-identifiers",
"objc2-user-notifications",
]
[[package]]
name = "objc2-uniform-type-identifiers"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
]
[[package]]
name = "objc2-user-notifications"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.9.0",
"block2",
"objc2",
"objc2-core-location",
"objc2-foundation",
]
[[package]]
@@ -12008,6 +11858,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"askpass",
"assistant_tool",
"assistant_tools",
"async-watch",
"backtrace",
"cargo_toml",
@@ -12030,6 +11882,7 @@ dependencies = [
"http_client",
"language",
"language_extension",
"language_model",
"languages",
"libc",
"log",
@@ -14744,6 +14597,7 @@ dependencies = [
"log",
"project",
"rand 0.8.5",
"regex",
"schemars",
"search",
"serde",
@@ -17085,18 +16939,22 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"component",
"db",
"documented",
"editor",
"fuzzy",
"gpui",
"install_cli",
"language",
"linkme",
"picker",
"project",
"schemars",
"serde",
"settings",
"telemetry",
"theme",
"ui",
"util",
"vim_mode_setting",
@@ -18148,6 +18006,7 @@ dependencies = [
"itertools 0.14.0",
"language",
"log",
"menu",
"node_runtime",
"parking_lot",
"postage",
@@ -18198,6 +18057,7 @@ dependencies = [
"base64ct",
"bigdecimal",
"bit-set 0.8.0",
"bit-vec 0.8.0",
"bitflags 2.9.0",
"bstr",
"bytemuck",
@@ -18209,6 +18069,7 @@ dependencies = [
"clang-sys",
"clap",
"clap_builder",
"codespan-reporting 0.12.0",
"concurrent-queue",
"core-foundation 0.9.4",
"core-foundation-sys",
@@ -18237,6 +18098,7 @@ dependencies = [
"getrandom 0.2.15",
"getrandom 0.3.2",
"gimli",
"half",
"handlebars 4.5.0",
"hashbrown 0.14.5",
"hashbrown 0.15.2",
@@ -18270,6 +18132,9 @@ dependencies = [
"num-iter",
"num-rational",
"num-traits",
"objc2",
"objc2-foundation",
"objc2-metal",
"object",
"once_cell",
"percent-encoding",
@@ -18288,6 +18153,7 @@ dependencies = [
"regex-syntax 0.8.5",
"ring",
"rust_decimal",
"rustc-hash 1.1.0",
"rustix 0.38.44",
"rustix 1.0.5",
"rustls 0.23.26",
@@ -18683,7 +18549,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.185.9"
version = "0.187.0"
dependencies = [
"activity_indicator",
"agent",
@@ -18691,7 +18557,6 @@ dependencies = [
"ashpd",
"askpass",
"assets",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_tools",
@@ -18873,9 +18738,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
version = "0.7.5"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe0d60001c02d0d21a4114a13bee3a905fbb9e146ada80a90435c05fda18852"
checksum = "a23b2fd00776b0c55072f389654910ceb501eb0083d7f78905ab0e5cc86949ec"
dependencies = [
"anyhow",
"serde",

View File

@@ -6,7 +6,6 @@ members = [
"crates/anthropic",
"crates/askpass",
"crates/assets",
"crates/assistant",
"crates/assistant_context_editor",
"crates/assistant_settings",
"crates/assistant_slash_command",
@@ -214,7 +213,6 @@ ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant_context_editor = { path = "crates/assistant_context_editor" }
assistant_settings = { path = "crates/assistant_settings" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
@@ -412,9 +410,9 @@ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-util = { git = "https://github.com/kvark/blade", rev = "b16f5c7bd873c7126f48c82c39e7ae64602ae74f" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -433,6 +431,7 @@ dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a0
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
dotenv = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
@@ -472,7 +471,7 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
markup5ever_rcdom = "0.3.0"
metal = "0.29"
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
naga = { version = "23.1.0", features = ["wgsl-in"] }
naga = { version = "25.0", features = ["wgsl-in"] }
nanoid = "0.4"
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
nix = "0.29"
@@ -609,7 +608,7 @@ wasmtime-wasi = "29"
which = "6.0.0"
wit-component = "0.221"
workspace-hack = "0.1.0"
zed_llm_client = "0.7.5"
zed_llm_client = "0.8.0"
zstd = "0.11"
[workspace.dependencies.async-stripe]
@@ -797,5 +796,6 @@ ignored = [
"serde",
"component",
"linkme",
"documented",
"workspace-hack",
]

View File

Before

Width:  |  Height:  |  Size: 474 B

After

Width:  |  Height:  |  Size: 474 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path fill-rule="evenodd" d="M4.5 2A1.5 1.5 0 0 0 3 3.5v13A1.5 1.5 0 0 0 4.5 18h11a1.5 1.5 0 0 0 1.5-1.5V7.621a1.5 1.5 0 0 0-.44-1.06l-4.12-4.122A1.5 1.5 0 0 0 11.378 2H4.5Zm2.25 8.5a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 3a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path d="M18.905 12.75a1.25 1.25 0 1 1-2.5 0v-7.5a1.25 1.25 0 0 1 2.5 0v7.5ZM8.905 17v1.3c0 .268-.14.526-.395.607A2 2 0 0 1 5.905 17c0-.995.182-1.948.514-2.826.204-.54-.166-1.174-.744-1.174h-2.52c-1.243 0-2.261-1.01-2.146-2.247.193-2.08.651-4.082 1.341-5.974C2.752 3.678 3.833 3 5.005 3h3.192a3 3 0 0 1 1.341.317l2.734 1.366A3 3 0 0 0 13.613 5h1.292v7h-.963c-.685 0-1.258.482-1.612 1.068a4.01 4.01 0 0 1-2.166 1.73c-.432.143-.853.386-1.011.814-.16.432-.248.9-.248 1.388Z" />
</svg>

Before

Width:  |  Height:  |  Size: 405 B

After

Width:  |  Height:  |  Size: 580 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
<path d="M1 8.25a1.25 1.25 0 1 1 2.5 0v7.5a1.25 1.25 0 1 1-2.5 0v-7.5ZM11 3V1.7c0-.268.14-.526.395-.607A2 2 0 0 1 14 3c0 .995-.182 1.948-.514 2.826-.204.54.166 1.174.744 1.174h2.52c1.243 0 2.261 1.01 2.146 2.247a23.864 23.864 0 0 1-1.341 5.974C17.153 16.323 16.072 17 14.9 17h-3.192a3 3 0 0 1-1.341-.317l-2.734-1.366A3 3 0 0 0 6.292 15H5V8h.963c.685 0 1.258-.483 1.612-1.068a4.011 4.011 0 0 1 2.166-1.73c.432-.143.853-.386 1.011-.814.16-.432.248-.9.248-1.388Z" />
</svg>

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 569 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-check-icon lucide-user-round-check"><path d="M2 21a8 8 0 0 1 13.292-6"/><circle cx="10" cy="8" r="5"/><path d="m16 19 2 2 4-4"/></svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -213,26 +213,10 @@
"ctrl-shift-n": "agent::RejectAll"
}
},
{
"context": "AssistantPanel",
"bindings": {
"ctrl-k c": "assistant::CopyCode",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-alt-/": "agent::ToggleModelSelector",
"ctrl-k h": "assistant::DeployHistory",
"ctrl-k l": "agent::OpenRulesLibrary",
"new": "assistant::NewChat",
"ctrl-t": "assistant::NewChat",
"ctrl-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-shift-enter": "assistant::Edit",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl->": "assistant::QuoteSelection",
@@ -713,8 +697,8 @@
{
"context": "PromptEditor",
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist",
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},
@@ -950,6 +934,7 @@
"ctrl-e": ["terminal::SendKeystroke", "ctrl-e"],
"ctrl-o": ["terminal::SendKeystroke", "ctrl-o"],
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
@@ -967,7 +952,10 @@
"shift-down": "terminal::ScrollLineDown",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode"
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-shift-r": "terminal::RerunTask",
"ctrl-alt-r": "terminal::RerunTask",
"alt-t": "terminal::RerunTask"
}
},
{
@@ -983,5 +971,12 @@
"enter": "editor::Newline",
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "Diagnostics",
"use_key_equivalents": true,
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
}
]

View File

@@ -258,27 +258,11 @@
"shift-ctrl-r": "agent::OpenAgentDiff"
}
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-k c": "assistant::CopyCode",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-g": "search::SelectNextMatch",
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-k h": "assistant::DeployHistory",
"cmd-k l": "agent::OpenRulesLibrary",
"cmd-t": "assistant::NewChat",
"cmd-n": "assistant::NewChat"
}
},
{
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-shift-enter": "assistant::Edit",
"cmd-s": "workspace::Save",
"cmd->": "assistant::QuoteSelection",
"cmd-<": "assistant::InsertIntoEditor",
@@ -780,8 +764,8 @@
"cmd-shift-a": "agent::ToggleContextPicker",
"cmd-alt-/": "agent::ToggleModelSelector",
"cmd-alt-e": "agent::RemoveAllContext",
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist"
}
},
{
@@ -1037,6 +1021,7 @@
"escape": ["terminal::SendKeystroke", "escape"],
"enter": ["terminal::SendKeystroke", "enter"],
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"shift-pageup": "terminal::ScrollPageUp",
"cmd-up": "terminal::ScrollPageUp",
"shift-pagedown": "terminal::ScrollPageDown",
@@ -1053,7 +1038,8 @@
"ctrl-alt-up": "pane::SplitUp",
"ctrl-alt-down": "pane::SplitDown",
"ctrl-alt-left": "pane::SplitLeft",
"ctrl-alt-right": "pane::SplitRight"
"ctrl-alt-right": "pane::SplitRight",
"cmd-alt-r": "terminal::RerunTask"
}
},
{
@@ -1091,5 +1077,12 @@
"enter": "editor::Newline",
"cmd-enter": "menu::Confirm"
}
},
{
"context": "Diagnostics",
"use_key_equivalents": true,
"bindings": {
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
}
}
]

View File

@@ -1,206 +0,0 @@
<task_description>
The user of a code editor wants to make a change to their codebase.
You must describe the change using the following XML structure:
- <patch> - A group of related code changes.
Child tags:
- <title> (required) - A high-level description of the changes. This should be as short
as possible, possibly using common abbreviations.
- <edit> (1 or more) - An edit to make at a particular range within a file.
Includes the following child tags:
- <path> (required) - The path to the file that will be changed.
- <description> (optional) - An arbitrarily-long comment that describes the purpose
of this edit.
- <old_text> (optional) - An excerpt from the file's current contents that uniquely
identifies a range within the file where the edit should occur. Required for all operations
except `create`.
- <new_text> (required) - The new text to insert into the file.
- <operation> (required) - The type of change that should occur at the given range
of the file. Must be one of the following values:
- `update`: Replaces the entire range with the new text.
- `insert_before`: Inserts the new text before the range.
- `insert_after`: Inserts new text after the range.
- `create`: Creates or overwrites a file with the given path and the new text.
- `delete`: Deletes the specified range from the file.
<guidelines>
- Never provide multiple edits whose ranges intersect each other. Instead, merge them into one edit.
- Prefer multiple edits to smaller, disjoint ranges, rather than one edit to a larger range.
- There's no need to escape angle brackets within XML tags.
- Always ensure imports are added if you're referencing symbols that are not in scope.
</guidelines>
Here are some concrete examples.
<example>
<message role="user">
```rs src/shapes.rs
pub mod rectangle;
pub mod circle;
```
```rs src/shapes/rectangle.rs
pub struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
}
```
```rs src/shapes/circle.rs
pub struct Circle {
radius: f64,
}
impl Circle {
pub fn new(radius: f64) -> Self {
Circle { radius }
}
}
```
Update all shapes to store their origin as an (x, y) tuple and implement Display.
</message>
<message role="assistant">
We'll need to update both the rectangle and circle modules.
<patch>
<title>Add origins and display impls to shapes</title>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>Add the origin field to Rectangle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Rectangle {
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>update</operation>
<old_text>
fn new(width: f64, height: f64) -> Self {
Rectangle { width, height }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), width: f64, height: f64) -> Self {
Rectangle { origin, width, height }
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<description>Add the origin field to Circle struct</description>
<operation>insert_after</operation>
<old_text>
pub struct Circle {
radius: f64,
</old_text>
<new_text>
origin: (f64, f64),
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>update</operation>
<old_text>
fn new(radius: f64) -> Self {
Circle { radius }
}
</old_text>
<new_text>
fn new(origin: (f64, f64), radius: f64) -> Self {
Circle { origin, radius }
}
</new_text>
</edit>
</step>
<edit>
<path>src/shapes/rectangle.rs</path>
<operation>insert_before</operation>
<old_text>
struct Rectangle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</edit>
<edit>
<path>src/shapes/rectangle.rs</path>
<description>
Add a manual Display implementation for Rectangle.
Currently, this is the same as a derived Display implementation.
</description>
<operation>insert_after</operation>
<old_text>
Rectangle { width, height }
}
}
</old_text>
<new_text>
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.format_struct(f, "Rectangle")
.field("origin", &self.origin)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_before</operation>
<old_text>
struct Circle {
</old_text>
<new_text>
use std::fmt;
</new_text>
</edit>
<edit>
<path>src/shapes/circle.rs</path>
<operation>insert_after</operation>
<old_text>
Circle { radius }
}
}
</old_text>
<new_text>
impl fmt::Display for Rectangle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.format_struct(f, "Rectangle")
.field("origin", &self.origin)
.field("width", &self.width)
.field("height", &self.height)
.finish()
}
}
</new_text>
</edit>
</patch>
</message>
</example>
</task_description>

View File

@@ -218,6 +218,23 @@
// 1. Do nothing: `none`
// 2. Find references for the same symbol: `find_all_references` (default)
"go_to_definition_fallback": "find_all_references",
// Which level to use to filter out diagnostics displayed in the editor.
//
// Affects the editor rendering only, and does not interrupt
// the functionality of diagnostics fetching and project diagnostics editor.
// Which files containing diagnostic errors/warnings to mark in the tabs.
// Diagnostics are only shown when file icons are also active.
// This setting only works when can take the following three values:
//
// Which diagnostic indicators to show in the scrollbar, their level should be more or equal to the specified severity level.
// Possible values:
// - "off" — no diagnostics are allowed
// - "error"
// - "warning" (default)
// - "info"
// - "hint"
// - null — allow all diagnostics
"diagnostics_max_severity": "warning",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -307,6 +324,15 @@
// Whether to show agent review buttons in the editor toolbar.
"agent_review": true
},
// Titlebar related settings
"title_bar": {
// Whether to show the branch icon beside branch switcher in the titlebar.
"show_branch_icon": false,
// Whether to show onboarding banners in the titlebar.
"show_onboarding_banner": true,
// Whether to show user picture in the titlebar.
"show_user_picture": true
},
// Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
@@ -347,6 +373,45 @@
"vertical": true
}
},
// Minimap related settings
"minimap": {
// When to show the minimap in the editor.
// This setting can take three values:
// 1. Show the minimap if the editor's scrollbar is visible:
// "auto"
// 2. Always show the minimap:
// "always"
// 3. Never show the minimap:
// "never" (default)
"show": "never",
// When to show the minimap thumb.
// This setting can take two values:
// 1. Show the minimap thumb if the mouse is over the minimap:
// "hover"
// 2. Always show the minimap thumb:
// "always" (default)
"thumb": "always",
// How the minimap thumb border should look.
// This setting can take five values:
// 1. Display a border on all sides of the thumb:
// "thumb_border": "full"
// 2. Display a border on all sides except the left side of the thumb:
// "thumb_border": "left_open" (default)
// 3. Display a border on all sides except the right side of the thumb:
// "thumb_border": "right_open"
// 4. Display a border only on the left side of the thumb:
// "thumb_border": "left_only"
// 5. Display the thumb without any border:
// "thumb_border": "none"
"thumb_border": "left_open",
// How to highlight the current line in the minimap.
// This setting can take the following values:
//
// 1. `null` to inherit the editor `current_line_highlight` setting (default)
// 2. "line" or "all" to highlight the current line in the minimap.
// 3. "gutter" or "none" to not highlight the current line in the minimap.
"current_line_highlight": null
},
// Enable middle-click paste on Linux.
"middle_click_paste": true,
// What to do when multibuffer is double clicked in some of its excerpts
@@ -361,8 +426,6 @@
"gutter": {
// Whether to show line numbers in the gutter.
"line_numbers": true,
// Whether to show code action buttons in the gutter.
"code_actions": true,
// Whether to show runnables buttons in the gutter.
"runnables": true,
// Whether to show breakpoints in the gutter.
@@ -860,7 +923,20 @@
// "modal_max_width": "full"
//
// Default: small
"modal_max_width": "small"
"modal_max_width": "small",
// Determines whether the file finder should skip focus for the active file in search results.
// There are 2 possible values:
//
// 1. true: When searching for files, if the currently active file appears as the first result,
// auto-focus will skip it and focus the second result instead.
// "skip_focus_for_active_in_search": true
//
// 2. false: When searching for files, the first result will always receive focus,
// even if it's the currently active file.
// "skip_focus_for_active_in_search": false
//
// Default: true
"skip_focus_for_active_in_search": true
},
// Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it.
@@ -912,6 +988,8 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// What debuggers are preferred by default for all languages.
"debuggers": [],
// Control what info is collected by Zed.
"telemetry": {
// Send debug info like crash reports.
@@ -941,8 +1019,13 @@
// longer than this value will still push diagnostics further to the right.
"min_column": 0,
// The minimum severity of the diagnostics to show inline.
// Shows all diagnostics when not specified.
// Inherits editor's diagnostics' max severity settings when `null`.
"max_severity": null
},
"cargo": {
// When enabled, Zed disables rust-analyzer's check on save and starts to query
// Cargo diagnostics separately.
"fetch_cargo_diagnostics": false
}
},
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
@@ -1326,6 +1409,9 @@
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
},
"Erlang": {
"language_servers": ["erlang-ls", "!elp", "..."]
},
@@ -1600,8 +1686,6 @@
// "W": "workspace::Save"
// }
"command_aliases": {},
// Whether to show user picture in titlebar.
"show_user_picture": true,
// ssh_connections is an array of ssh connections.
// You can configure these from `project: Open Remote` in the command palette.
// Zed's ssh support will pull configuration from your ~/.ssh too.

View File

@@ -43,6 +43,7 @@ pub struct ActivityIndicator {
context_menu_handle: PopoverMenuHandle<ContextMenu>,
}
#[derive(Debug)]
struct ServerStatus {
name: SharedString,
status: BinaryStatus,
@@ -70,6 +71,7 @@ impl ActivityIndicator {
) -> Entity<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
let workspace_handle = cx.entity();
let this = cx.new(|cx| {
let mut status_events = languages.language_server_binary_statuses();
cx.spawn(async move |this, cx| {
@@ -84,17 +86,23 @@ impl ActivityIndicator {
})
.detach();
let mut status_events = languages.dap_server_binary_statuses();
cx.spawn(async move |this, cx| {
while let Some((name, status)) = status_events.next().await {
this.update(cx, |this, cx| {
this.statuses.retain(|s| s.name != name);
this.statuses.push(ServerStatus { name, status });
cx.notify();
})?;
}
anyhow::Ok(())
})
cx.subscribe_in(
&workspace_handle,
window,
|activity_indicator, _, event, window, cx| match event {
workspace::Event::ClearActivityIndicator { .. } => {
if activity_indicator.statuses.pop().is_some() {
activity_indicator.dismiss_error_message(
&DismissErrorMessage,
window,
cx,
);
cx.notify();
}
}
_ => {}
},
)
.detach();
cx.subscribe(
@@ -128,7 +136,7 @@ impl ActivityIndicator {
}
Self {
statuses: Default::default(),
statuses: Vec::new(),
project: project.clone(),
auto_updater,
context_menu_handle: Default::default(),
@@ -198,11 +206,8 @@ impl ActivityIndicator {
cx: &mut Context<Self>,
) {
if let Some(updater) = &self.auto_updater {
updater.update(cx, |updater, cx| {
updater.dismiss_error(cx);
});
updater.update(cx, |updater, cx| updater.dismiss_error(cx));
}
cx.notify();
}
fn pending_language_server_work<'a>(

View File

@@ -9,7 +9,7 @@ license = "GPL-3.0-or-later"
workspace = true
[lib]
path = "src/assistant.rs"
path = "src/agent.rs"
doctest = false
[features]
@@ -23,6 +23,7 @@ anyhow.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
buffer_diff.workspace = true
@@ -45,6 +46,7 @@ gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indexed_docs.workspace = true
itertools.workspace = true
jsonschema.workspace = true
language.workspace = true

View File

@@ -1,4 +1,4 @@
use crate::AssistantPanel;
use crate::AgentPanel;
use crate::context::{AgentContextHandle, RULES_ICON};
use crate::context_picker::{ContextPicker, MentionLink};
use crate::context_store::ContextStore;
@@ -43,7 +43,6 @@ use std::sync::Arc;
use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
Disclosure, IconButton, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
@@ -713,7 +712,7 @@ fn open_markdown_link(
.detach_and_log_err(cx);
}
Some(MentionLink::Thread(thread_id)) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_thread_by_id(&thread_id, window, cx)
@@ -722,7 +721,7 @@ fn open_markdown_link(
}
}),
Some(MentionLink::TextThread(path)) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_saved_prompt_editor(path, window, cx)
@@ -1212,8 +1211,7 @@ impl ActiveThread {
if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(_cx, |workspace, cx| {
workspace
.focus_panel::<AssistantPanel>(window, cx);
workspace.focus_panel::<AgentPanel>(window, cx);
});
}
})
@@ -1283,9 +1281,6 @@ impl ActiveThread {
return;
};
// Cancel any ongoing streaming when user starts editing a previous message
self.cancel_last_completion(window, cx);
let editor = crate::message_editor::create_editor(
self.workspace.clone(),
self.context_store.downgrade(),
@@ -1416,6 +1411,7 @@ impl ActiveThread {
mode: None,
messages: vec![request_message],
tools: vec![],
tool_choice: None,
stop: vec![],
temperature: AssistantSettings::temperature_for_model(
&configured_model.model,
@@ -1784,8 +1780,7 @@ impl ActiveThread {
let colors = cx.theme().colors();
let editor_bg_color = colors.editor_background;
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::FileCode)
.shape(ui::IconButtonShape::Square)
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.tooltip(Tooltip::text("Open Thread as Markdown"))
@@ -1810,13 +1805,16 @@ impl ActiveThread {
.mt_1()
.py_2()
.px(RESPONSE_PADDING_X)
.gap_1()
.mr_1()
.opacity(0.4)
.hover(|style| style.opacity(1.))
.gap_1p5()
.flex_wrap()
.justify_end();
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
Some(feedback) => feedback_container
.child(
div().mr_1().visible_on_hover("feedback_container").child(
div().visible_on_hover("feedback_container").child(
Label::new(match feedback {
ThreadFeedback::Positive => "Thanks for your feedback!",
ThreadFeedback::Negative => {
@@ -1829,11 +1827,8 @@ impl ActiveThread {
)
.child(
h_flex()
.pr_1()
.gap_1()
.child(
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(match feedback {
ThreadFeedback::Positive => Color::Accent,
@@ -1851,7 +1846,6 @@ impl ActiveThread {
)
.child(
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
.shape(ui::IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(match feedback {
ThreadFeedback::Positive => Color::Ignored,
@@ -1872,7 +1866,7 @@ impl ActiveThread {
.into_any_element(),
None => feedback_container
.child(
div().mr_1().visible_on_hover("feedback_container").child(
div().visible_on_hover("feedback_container").child(
Label::new(
"Rating the thread sends all of your current conversation to the Zed team.",
)
@@ -1882,13 +1876,10 @@ impl ActiveThread {
)
.child(
h_flex()
.pr_1()
.gap_1()
.child(
IconButton::new(("feedback-thumbs-up", ix), IconName::ThumbsUp)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Helpful Response"))
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(
@@ -1903,7 +1894,6 @@ impl ActiveThread {
IconButton::new(("feedback-thumbs-down", ix), IconName::ThumbsDown)
.icon_size(IconSize::XSmall)
.icon_color(Color::Ignored)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Not Helpful"))
.on_click(cx.listener(move |this, _, window, cx| {
this.handle_feedback_click(
@@ -2079,202 +2069,185 @@ impl ActiveThread {
let panel_background = cx.theme().colors().panel_background;
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
.size_full()
.child(
v_flex()
.w_full()
.map(|parent| {
if let Some(checkpoint) = checkpoint.filter(|_| !is_generating) {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
}
}
v_flex()
.w_full()
.map(|parent| {
if let Some(checkpoint) = checkpoint.filter(|_| !is_generating) {
let mut is_pending = false;
let mut error = None;
if let Some(last_restore_checkpoint) =
self.thread.read(cx).last_restore_checkpoint()
{
if last_restore_checkpoint.message_id() == message_id {
match last_restore_checkpoint {
LastRestoreCheckpoint::Pending { .. } => is_pending = true,
LastRestoreCheckpoint::Error { error: err, .. } => {
error = Some(err.clone());
}
}
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.label_size(LabelSize::XSmall)
.disabled(is_pending)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(
h_flex()
.pt_2p5()
.px_2p5()
.w_full()
.gap_1()
.child(ui::Divider::horizontal())
.child(restore_checkpoint_button)
.child(ui::Divider::horizontal()),
)
} else {
parent
}
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| {
this.child(loading_dots)
}),
)
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
move |parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(
move |this, _: &menu::Cancel, _, cx| {
this.open_feedback_editors.remove(&message_id);
cx.notify();
},
))
.on_action(cx.listener(
move |this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify();
},
))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor.clone())
}
let restore_checkpoint_button =
Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
.icon(if error.is_some() {
IconName::XCircle
} else {
IconName::Undo
})
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::Start)
.icon_color(if error.is_some() {
Some(Color::Error)
} else {
None
})
.label_size(LabelSize::XSmall)
.disabled(is_pending)
.on_click(cx.listener(move |this, _, _window, cx| {
this.thread.update(cx, |thread, cx| {
thread
.restore_checkpoint(checkpoint.clone(), cx)
.detach_and_log_err(cx);
});
}));
let restore_checkpoint_button = if is_pending {
restore_checkpoint_button
.with_animation(
("pulsating-restore-checkpoint-button", ix),
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.6, 1.)),
|label, delta| label.alpha(delta),
)
.into_any_element()
} else if let Some(error) = error {
restore_checkpoint_button
.tooltip(Tooltip::text(error.to_string()))
.into_any_element()
} else {
restore_checkpoint_button.into_any_element()
};
parent.child(
h_flex()
.pt_2p5()
.px_2p5()
.w_full()
.gap_1()
.child(ui::Divider::horizontal())
.child(restore_checkpoint_button)
.child(ui::Divider::horizontal()),
)
} else {
parent
}
})
.when(is_first_message, |parent| {
parent.child(self.render_rules_item(cx))
})
.child(styled_message)
.when(is_generating && is_last_message, |this| {
this.child(
h_flex()
.h_8()
.mt_2()
.mb_4()
.ml_4()
.py_1p5()
.when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
)
})
.when(show_feedback, move |parent| {
parent.child(feedback_items).when_some(
self.open_feedback_editors.get(&message_id),
move |parent, feedback_editor| {
let focus_handle = feedback_editor.focus_handle(cx);
parent.child(
v_flex()
.key_context("AgentFeedbackMessageEditor")
.on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| {
this.open_feedback_editors.remove(&message_id);
cx.notify();
}))
.on_action(cx.listener(move |this, _: &menu::Confirm, _, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify();
}))
.on_action(cx.listener(Self::confirm_editing_message))
.mb_2()
.mx_4()
.p_2()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.child(feedback_editor.clone())
.child(
h_flex()
.gap_1()
.justify_end()
.child(
h_flex()
.gap_1()
.justify_end()
.child(
Button::new(
"dismiss-feedback-message",
"Cancel",
Button::new("dismiss-feedback-message", "Cancel")
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Cancel,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.open_feedback_editors
.remove(&message_id);
cx.notify();
},
)),
.map(|kb| kb.size(rems_from_px(10.))),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(
ui::TintColor::Accent,
))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(cx.listener(
move |this, _, _window, cx| {
this.submit_feedback_message(
message_id, cx,
);
cx.notify()
},
)),
),
.on_click(cx.listener(
move |this, _, _window, cx| {
this.open_feedback_editors
.remove(&message_id);
cx.notify();
},
)),
)
.child(
Button::new(
"submit-feedback-message",
"Share Feedback",
)
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&menu::Confirm,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(10.))),
)
.on_click(
cx.listener(move |this, _, _window, cx| {
this.submit_feedback_message(message_id, cx);
cx.notify()
}),
),
),
)
},
),
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
}),
)
},
)
})
.when(after_editing_message, |parent| {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.absolute()
.inset_0()
.size_full()
.bg(panel_background)
.opacity(0.8),
)
})
.into_any()
}
@@ -3279,15 +3252,18 @@ impl ActiveThread {
.map(|tool_use| tool_use.status.clone())
{
self.thread.update(cx, |thread, cx| {
thread.run_tool(
c.tool_use_id.clone(),
c.ui_text.clone(),
c.input.clone(),
&c.messages,
c.tool.clone(),
Some(window.window_handle()),
cx,
);
if let Some(configured) = thread.get_or_init_configured_model(cx) {
thread.run_tool(
c.tool_use_id.clone(),
c.ui_text.clone(),
c.input.clone(),
c.request.clone(),
c.tool.clone(),
configured.model,
Some(window.window_handle()),
cx,
);
}
});
}
}
@@ -3466,6 +3442,11 @@ pub(crate) fn open_active_thread_as_markdown(
.unwrap_or_else(|| "Thread".to_string());
let project = workspace.project().clone();
if !project.read(cx).is_local() {
anyhow::bail!("failed to open active thread as markdown in remote project");
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), cx)
});
@@ -3543,7 +3524,7 @@ pub(crate) fn open_context(
}
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_thread(thread_context.thread.clone(), window, cx);
});
@@ -3552,7 +3533,7 @@ pub(crate) fn open_context(
AgentContextHandle::TextThread(text_thread_context) => {
workspace.update(cx, |workspace, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
});
@@ -3596,152 +3577,3 @@ fn open_editor_at_position(
}
})
}
#[cfg(test)]
mod tests {
use assistant_tool::{ToolRegistry, ToolWorkingSet};
use editor::EditorSettings;
use fs::FakeFs;
use gpui::{TestAppContext, VisualTestContext};
use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
use project::Project;
use prompt_store::PromptBuilder;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use crate::{ContextLoadResult, thread_store};
use super::*;
#[gpui::test]
async fn test_current_completion_cancelled_when_message_edited(cx: &mut TestAppContext) {
init_test_settings(cx);
let project = create_test_project(
cx,
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
)
.await;
let (cx, active_thread, thread, model) = setup_test_environment(cx, project.clone()).await;
// Insert user message without any context (empty context vector)
let message = thread.update(cx, |thread, cx| {
let message_id = thread.insert_user_message(
"What is the best way to learn Rust?",
ContextLoadResult::default(),
None,
vec![],
cx,
);
thread
.message(message_id)
.expect("message should exist")
.clone()
});
// Stream response to user message
thread.update(cx, |thread, cx| {
let request = thread.to_completion_request(model.clone(), cx);
thread.stream_completion(request, model, cx.active_window(), cx)
});
let generating = thread.update(cx, |thread, _cx| thread.is_generating());
assert!(generating, "There should be one pending completion");
// Edit the previous message
active_thread.update_in(cx, |active_thread, window, cx| {
active_thread.start_editing_message(message.id, &message.segments, window, cx);
});
// Check that the stream was cancelled
let generating = thread.update(cx, |thread, _cx| thread.is_generating());
assert!(!generating, "The completion should have been cancelled");
}
fn init_test_settings(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AssistantSettings::register(cx);
prompt_store::init(cx);
thread_store::init(cx);
workspace::init_settings(cx);
language_model::init_settings(cx);
ThemeSettings::register(cx);
EditorSettings::register(cx);
ToolRegistry::default_global(cx);
});
}
// Helper to create a test project with test files
async fn create_test_project(
cx: &mut TestAppContext,
files: serde_json::Value,
) -> Entity<Project> {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), files).await;
Project::test(fs, [path!("/test").as_ref()], cx).await
}
async fn setup_test_environment(
cx: &mut TestAppContext,
project: Entity<Project>,
) -> (
&mut VisualTestContext,
Entity<ActiveThread>,
Entity<Thread>,
Arc<dyn LanguageModel>,
) {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let thread_store = cx
.update(|_, cx| {
ThreadStore::load(
project.clone(),
cx.new(|_| ToolWorkingSet::default()),
None,
prompt_builder.clone(),
cx,
)
})
.await
.unwrap();
let text_thread_store = cx
.update(|_, cx| {
TextThreadStore::new(project.clone(), prompt_builder, Default::default(), cx)
})
.await
.unwrap();
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None));
let model = FakeLanguageModel::default();
let model: Arc<dyn LanguageModel> = Arc::new(model);
let language_registry = LanguageRegistry::new(cx.executor());
let language_registry = Arc::new(language_registry);
let active_thread = cx.update(|window, cx| {
cx.new(|cx| {
ActiveThread::new(
thread.clone(),
thread_store.clone(),
text_thread_store.clone(),
context_store.clone(),
language_registry.clone(),
workspace.downgrade(),
window,
cx,
)
})
});
(cx, active_thread, thread, model)
}
}

View File

@@ -1,102 +1,123 @@
#![cfg_attr(target_os = "windows", allow(unused, dead_code))]
mod assistant_configuration;
pub mod assistant_panel;
mod active_thread;
mod agent_configuration;
mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
mod context;
mod context_picker;
mod context_server_configuration;
mod context_server_tool;
mod context_store;
mod context_strip;
mod debug;
mod history_store;
mod inline_assistant;
pub mod slash_command_settings;
mod inline_prompt_editor;
mod message_editor;
mod profile_selector;
mod slash_command_settings;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_compatibility;
mod tool_use;
mod ui;
use std::sync::Arc;
use assistant_settings::{AssistantSettings, LanguageModelSelection};
use assistant_settings::{AgentProfileId, AssistantSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::FeatureFlagAppExt;
use feature_flags::FeatureFlagAppExt as _;
use fs::Fs;
use gpui::{App, Global, ReadGlobal, UpdateGlobal, actions};
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
use gpui::{App, actions, impl_actions};
use language::LanguageRegistry;
use language_model::{LanguageModelId, LanguageModelProviderId, LanguageModelRegistry};
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{Settings, SettingsStore};
use settings::{Settings as _, SettingsStore};
use thread::ThreadId;
pub use crate::assistant_panel::{AssistantPanel, AssistantPanelEvent};
pub(crate) use crate::inline_assistant::*;
pub use crate::active_thread::ActiveThread;
use crate::agent_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
assistant,
agent,
[
InsertActivePrompt,
DeployHistory,
NewChat,
NewTextThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
DeleteRecentlyOpenThread,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,
OpenHistory,
AddContextServer,
RemoveSelectedThread,
Chat,
CycleNextInlineAssist,
CyclePreviousInlineAssist
CyclePreviousInlineAssist,
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown,
OpenAgentDiff,
Keep,
Reject,
RejectAll,
KeepAll,
Follow,
ResetTrialUpsell,
]
);
const DEFAULT_CONTEXT_LINES: usize = 50;
#[derive(Deserialize, Debug)]
pub struct LanguageModelUsage {
pub prompt_tokens: u32,
pub completion_tokens: u32,
pub total_tokens: u32,
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
}
#[derive(Deserialize, Debug)]
pub struct LanguageModelChoiceDelta {
pub index: u32,
pub delta: LanguageModelResponseMessage,
pub finish_reason: Option<String>,
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
}
/// The state pertaining to the Assistant.
#[derive(Default)]
struct Assistant {
/// Whether the Assistant is enabled.
enabled: bool,
}
impl Global for Assistant {}
impl Assistant {
const NAMESPACE: &'static str = "assistant";
fn set_enabled(&mut self, enabled: bool, cx: &mut App) {
if self.enabled == enabled {
return;
impl ManageProfiles {
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
Self {
customize_tools: Some(profile_id),
}
self.enabled = enabled;
if !enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Self::NAMESPACE);
});
return;
}
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(Self::NAMESPACE);
});
}
pub fn enabled(cx: &App) -> bool {
Self::global(cx).enabled
}
}
impl_actions!(agent, [NewThread, ManageProfiles]);
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
cx.set_global(Assistant::default());
AssistantSettings::register(cx);
SlashCommandSettings::register(cx);
@@ -104,8 +125,9 @@ pub fn init(
rules_library::init(cx);
init_language_model_settings(cx);
assistant_slash_command::init(cx);
assistant_tool::init(cx);
assistant_panel::init(cx);
thread_store::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry, cx);
register_slash_commands(cx);
inline_assistant::init(
@@ -121,22 +143,8 @@ pub fn init(
cx,
);
indexed_docs::init(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
});
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
cx.observe_global::<SettingsStore>(|cx| {
Assistant::update_global(cx, |assistant, cx| {
let settings = AssistantSettings::get_global(cx);
assistant.set_enabled(settings.enabled, cx);
});
})
.detach();
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
}
fn init_language_model_settings(cx: &mut App) {
@@ -250,11 +258,3 @@ fn update_slash_commands_from_settings(cx: &mut App) {
.unregister_command(assistant_slash_commands::CargoWorkspaceSlashCommand);
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}

View File

@@ -30,7 +30,7 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::AddContextServer;
pub struct AssistantConfiguration {
pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -42,7 +42,7 @@ pub struct AssistantConfiguration {
scrollbar_state: ScrollbarState,
}
impl AssistantConfiguration {
impl AgentConfiguration {
pub fn new(
fs: Arc<dyn Fs>,
context_server_store: Entity<ContextServerStore>,
@@ -110,7 +110,7 @@ impl AssistantConfiguration {
}
}
impl Focusable for AssistantConfiguration {
impl Focusable for AgentConfiguration {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
@@ -120,9 +120,9 @@ pub enum AssistantConfigurationEvent {
NewThread(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<AssistantConfigurationEvent> for AssistantConfiguration {}
impl EventEmitter<AssistantConfigurationEvent> for AgentConfiguration {}
impl AssistantConfiguration {
impl AgentConfiguration {
fn render_provider_configuration_block(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
@@ -571,7 +571,7 @@ impl AssistantConfiguration {
}
}
impl Render for AssistantConfiguration {
impl Render for AgentConfiguration {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.id("assistant-configuration")

View File

@@ -18,9 +18,9 @@ use ui::{
use util::ResultExt as _;
use workspace::{ModalView, Workspace};
use crate::assistant_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AssistantPanel, ManageProfiles, ThreadStore};
use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
use crate::{AgentPanel, ManageProfiles, ThreadStore};
use super::tool_picker::ToolPickerMode;
@@ -115,7 +115,7 @@ impl ManageProfilesModal {
_cx: &mut Context<Workspace>,
) {
workspace.register_action(|workspace, action: &ManageProfiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
let fs = workspace.app_state().fs.clone();
let thread_store = panel.read(cx).thread_store();
let tools = thread_store.read(cx).tools();
@@ -124,7 +124,7 @@ impl ManageProfilesModal {
let mut this = Self::new(fs, tools, thread_store, window, cx);
if let Some(profile_id) = action.customize_tools.clone() {
this.configure_tools(profile_id, window, cx);
this.configure_builtin_tools(profile_id, window, cx);
}
this
@@ -190,7 +190,7 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
fn configure_mcps(
fn configure_mcp_tools(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
@@ -228,7 +228,7 @@ impl ManageProfilesModal {
self.focus_handle(cx).focus(window);
}
fn configure_tools(
fn configure_builtin_tools(
&mut self,
profile_id: AgentProfileId,
window: &mut Window,
@@ -581,16 +581,20 @@ impl ManageProfilesModal {
)
.child(
div()
.id("configure-tools")
.id("configure-builtin-tools")
.track_focus(&mode.configure_tools.focus_handle)
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_tools(profile_id.clone(), window, cx);
this.configure_builtin_tools(
profile_id.clone(),
window,
cx,
);
})
})
.child(
ListItem::new("configure-tools")
ListItem::new("configure-builtin-tools-item")
.toggle_state(
mode.configure_tools
.focus_handle
@@ -603,11 +607,11 @@ impl ManageProfilesModal {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure Tools"))
.child(Label::new("Configure Built-in Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_tools(
this.configure_builtin_tools(
profile_id.clone(),
window,
cx,
@@ -623,11 +627,11 @@ impl ManageProfilesModal {
.on_action({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _: &menu::Confirm, window, cx| {
this.configure_mcps(profile_id.clone(), window, cx);
this.configure_mcp_tools(profile_id.clone(), window, cx);
})
})
.child(
ListItem::new("configure-mcps")
ListItem::new("configure-mcp-tools")
.toggle_state(
mode.configure_mcps
.focus_handle
@@ -640,11 +644,15 @@ impl ManageProfilesModal {
.size(IconSize::Small)
.color(Color::Muted),
)
.child(Label::new("Configure MCP Servers"))
.child(Label::new("Configure MCP Tools"))
.on_click({
let profile_id = mode.profile_id.clone();
cx.listener(move |this, _, window, cx| {
this.configure_mcps(profile_id.clone(), window, cx);
this.configure_mcp_tools(
profile_id.clone(),
window,
cx,
);
})
}),
),
@@ -777,7 +785,7 @@ impl Render for ManageProfilesModal {
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure Tools"),
format!("{profile_name} — Configure Built-in Tools"),
Some(IconName::Cog),
))
.child(ListSeparator)
@@ -800,7 +808,7 @@ impl Render for ManageProfilesModal {
v_flex()
.pb_1()
.child(ProfileModalHeader::new(
format!("{profile_name} — Configure MCP Servers"),
format!("{profile_name} — Configure MCP Tools"),
Some(IconName::Hammer),
))
.child(ListSeparator)

View File

@@ -176,7 +176,7 @@ impl PickerDelegate for ToolPickerDelegate {
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
match self.mode {
ToolPickerMode::BuiltinTools => "Search built-in tools…",
ToolPickerMode::McpTools => "Search MCP servers…",
ToolPickerMode::McpTools => "Search MCP tools…",
}
.into()
}

View File

@@ -17,13 +17,13 @@ pub enum ModelType {
InlineAssistant,
}
pub struct AssistantModelSelector {
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
focus_handle: FocusHandle,
}
impl AssistantModelSelector {
impl AgentModelSelector {
pub(crate) fn new(
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<LanguageModelSelector>,
@@ -99,7 +99,7 @@ impl AssistantModelSelector {
}
}
impl Render for AssistantModelSelector {
impl Render for AgentModelSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use anyhow::{Result, anyhow};
use assistant_context_editor::{
AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
render_remaining_tokens,
};
@@ -39,6 +39,7 @@ use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle,
ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
@@ -52,8 +53,8 @@ use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFon
use zed_llm_client::UsageLimit;
use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
use crate::agent_configuration::{AgentConfiguration, AssistantConfigurationEvent};
use crate::agent_diff::AgentDiff;
use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
@@ -70,7 +71,7 @@ use crate::{
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize)]
struct SerializedAssistantPanel {
struct SerializedAgentPanel {
width: Option<Pixels>,
}
@@ -79,40 +80,40 @@ pub fn init(cx: &mut App) {
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace
.register_action(|workspace, action: &NewThread, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
workspace.focus_panel::<AssistantPanel>(window, cx);
workspace.focus_panel::<AgentPanel>(window, cx);
}
})
.register_action(|workspace, _: &OpenHistory, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
}
})
.register_action(|workspace, _: &NewTextThread, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
}
})
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.deploy_rules_library(action, window, cx)
});
}
})
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
let thread = panel.read(cx).thread.read(cx).thread().clone();
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
}
@@ -121,8 +122,8 @@ pub fn init(cx: &mut App) {
workspace.follow(CollaboratorId::Agent, window, cx);
})
.register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.message_editor.update(cx, |editor, cx| {
editor.expand_message_editor(&ExpandMessageEditor, window, cx);
@@ -131,16 +132,16 @@ pub fn init(cx: &mut App) {
}
})
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
});
}
})
.register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
});
@@ -177,7 +178,21 @@ enum ActiveView {
Configuration,
}
enum WhichFontSize {
AgentFont,
BufferFont,
None,
}
impl ActiveView {
pub fn which_font_size_used(&self) -> WhichFontSize {
match self {
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
ActiveView::PromptEditor { .. } => WhichFontSize::BufferFont,
ActiveView::Configuration => WhichFontSize::None,
}
}
pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
let summary = thread.read(cx).summary_or_default();
@@ -320,7 +335,7 @@ impl ActiveView {
}
}
pub struct AssistantPanel {
pub struct AgentPanel {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
project: Entity<Project>,
@@ -334,7 +349,7 @@ pub struct AssistantPanel {
context_store: Entity<TextThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
inline_assist_context_store: Entity<crate::context_store::ContextStore>,
configuration: Option<Entity<AssistantConfiguration>>,
configuration: Option<Entity<AgentConfiguration>>,
configuration_subscription: Option<Subscription>,
local_timezone: UtcOffset,
active_view: ActiveView,
@@ -351,14 +366,14 @@ pub struct AssistantPanel {
_trial_markdown: Entity<Markdown>,
}
impl AssistantPanel {
impl AgentPanel {
fn serialize(&mut self, cx: &mut Context<Self>) {
let width = self.width;
self.pending_serialization = Some(cx.background_spawn(async move {
KEY_VALUE_STORE
.write_kvp(
AGENT_PANEL_KEY.into(),
serde_json::to_string(&SerializedAssistantPanel { width })?,
serde_json::to_string(&SerializedAgentPanel { width })?,
)
.await?;
anyhow::Ok(())
@@ -408,7 +423,7 @@ impl AssistantPanel {
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
} else {
None
};
@@ -476,7 +491,6 @@ impl AssistantPanel {
thread_store.downgrade(),
context_store.downgrade(),
thread.clone(),
agent_panel_dock_position(cx),
window,
cx,
)
@@ -495,6 +509,7 @@ impl AssistantPanel {
thread_store.clone(),
context_store.clone(),
[RecentEntry::Thread(thread_id, thread.clone())],
window,
cx,
)
});
@@ -749,9 +764,9 @@ impl AssistantPanel {
});
if let Some(other_thread_id) = action.from_thread_id.clone() {
let other_thread_task = self
.thread_store
.update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
let other_thread_task = self.thread_store.update(cx, |this, cx| {
this.open_thread(&other_thread_id, window, cx)
});
cx.spawn({
let context_store = context_store.clone();
@@ -806,7 +821,6 @@ impl AssistantPanel {
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread,
agent_panel_dock_position(cx),
window,
cx,
)
@@ -952,7 +966,7 @@ impl AssistantPanel {
) -> Task<Result<()>> {
let open_thread_task = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx));
.update(cx, |this, cx| this.open_thread(thread_id, window, cx));
cx.spawn_in(window, async move |this, cx| {
let thread = open_thread_task.await?;
this.update_in(cx, |this, window, cx| {
@@ -1015,7 +1029,6 @@ impl AssistantPanel {
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread,
agent_panel_dock_position(cx),
window,
cx,
)
@@ -1072,7 +1085,7 @@ impl AssistantPanel {
_: &mut Window,
cx: &mut Context<Self>,
) {
self.adjust_font_size(action.persist, px(1.0), cx);
self.handle_font_size_action(action.persist, px(1.0), cx);
}
pub fn decrease_font_size(
@@ -1081,21 +1094,36 @@ impl AssistantPanel {
_: &mut Window,
cx: &mut Context<Self>,
) {
self.adjust_font_size(action.persist, px(-1.0), cx);
self.handle_font_size_action(action.persist, px(-1.0), cx);
}
fn adjust_font_size(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
if persist {
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, cx| {
let agent_font_size = ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).0);
});
} else {
theme::adjust_agent_font_size(cx, |size| {
*size += delta;
});
fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
match self.active_view.which_font_size_used() {
WhichFontSize::AgentFont => {
if persist {
update_settings_file::<ThemeSettings>(
self.fs.clone(),
cx,
move |settings, cx| {
let agent_font_size =
ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
let _ = settings
.agent_font_size
.insert(theme::clamp_font_size(agent_font_size).0);
},
);
} else {
theme::adjust_agent_font_size(cx, |size| {
*size += delta;
});
}
}
WhichFontSize::BufferFont => {
// Prompt editor uses the buffer font size, so allow the action to propagate to the
// default handler that changes that font size.
cx.propagate();
}
WhichFontSize::None => {}
}
}
@@ -1135,15 +1163,13 @@ impl AssistantPanel {
self.set_active_view(ActiveView::Configuration, window, cx);
self.configuration =
Some(cx.new(|cx| {
AssistantConfiguration::new(fs, context_server_store, tools, window, cx)
}));
Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
if let Some(configuration) = self.configuration.as_ref() {
self.configuration_subscription = Some(cx.subscribe_in(
configuration,
window,
Self::handle_assistant_configuration_event,
Self::handle_agent_configuration_event,
));
configuration.focus_handle(cx).focus(window);
@@ -1173,9 +1199,9 @@ impl AssistantPanel {
.detach_and_log_err(cx);
}
fn handle_assistant_configuration_event(
fn handle_agent_configuration_event(
&mut self,
_entity: &Entity<AssistantConfiguration>,
_entity: &Entity<AgentConfiguration>,
event: &AssistantConfigurationEvent,
window: &mut Window,
cx: &mut Context<Self>,
@@ -1288,7 +1314,7 @@ impl AssistantPanel {
}
}
impl Focusable for AssistantPanel {
impl Focusable for AgentPanel {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.active_view {
ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
@@ -1313,9 +1339,9 @@ fn agent_panel_dock_position(cx: &App) -> DockPosition {
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl EventEmitter<PanelEvent> for AgentPanel {}
impl Panel for AssistantPanel {
impl Panel for AgentPanel {
fn persistent_name() -> &'static str {
"AgentPanel"
}
@@ -1329,10 +1355,6 @@ impl Panel for AssistantPanel {
}
fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
self.message_editor.update(cx, |message_editor, cx| {
message_editor.set_dock_position(position, cx);
});
settings::update_settings_file::<AssistantSettings>(
self.fs.clone(),
cx,
@@ -1394,7 +1416,7 @@ impl Panel for AssistantPanel {
}
}
impl AssistantPanel {
impl AgentPanel {
fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
@@ -1953,9 +1975,9 @@ impl AssistantPanel {
.style(ButtonStyle::Transparent)
.color(Color::Muted)
.on_click({
let assistant_panel = cx.entity();
let agent_panel = cx.entity();
move |_, _, cx| {
assistant_panel.update(
agent_panel.update(
cx,
|this, cx| {
let hidden =
@@ -2314,14 +2336,13 @@ impl AssistantPanel {
""
};
Some(
Banner::new()
.severity(ui::Severity::Info)
.child(h_flex().child(Label::new(format!(
"Consecutive tool use limit reached.{max_mode_upsell}"
))))
.into_any_element(),
)
let banner = Banner::new()
.severity(ui::Severity::Info)
.child(h_flex().child(Label::new(format!(
"Consecutive tool use limit reached.{max_mode_upsell}"
))));
Some(div().px_2().pb_2().child(banner).into_any_element())
}
fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
@@ -2560,6 +2581,46 @@ impl AssistantPanel {
.into_any()
}
fn render_prompt_editor(
&self,
context_editor: &Entity<ContextEditor>,
buffer_search_bar: &Entity<BufferSearchBar>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Div {
let mut registrar = buffer_search::DivRegistrar::new(
|this, _, _cx| match &this.active_view {
ActiveView::PromptEditor {
buffer_search_bar, ..
} => Some(buffer_search_bar.clone()),
_ => None,
},
cx,
);
BufferSearchBar::register(&mut registrar);
registrar
.into_div()
.size_full()
.relative()
.map(|parent| {
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if buffer_search_bar.is_dismissed() {
return parent;
}
parent.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(buffer_search_bar.render(window, cx)),
)
})
})
.child(context_editor.clone())
.child(self.render_drag_target(cx))
}
fn render_drag_target(&self, cx: &Context<Self>) -> Div {
let is_local = self.project.read(cx).is_local();
div()
@@ -2681,9 +2742,18 @@ impl AssistantPanel {
}
}
impl Render for AssistantPanel {
impl Render for AgentPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
// WARNING: Changes to this element hierarchy can have
// non-obvious implications to the layout of children.
//
// If you need to change it, please confirm:
// - The message editor expands (⌘esc) correctly
// - When expanded, the buttons at the bottom of the panel are displayed correctly
// - Font size works as expected and can be changed with ⌘+/⌘-
// - Scrolling in all views works as expected
// - Files can be dropped into the panel
let content = v_flex()
.key_context(self.key_context())
.justify_between()
.size_full()
@@ -2709,59 +2779,36 @@ impl Render for AssistantPanel {
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.map(|parent| match &self.active_view {
ActiveView::Thread { .. } => parent.child(
v_flex()
.relative()
.justify_between()
.size_full()
.child(self.render_active_thread_or_empty_state(window, cx))
.children(self.render_tool_use_limit_reached(cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx))
.child(self.render_drag_target(cx)),
),
ActiveView::Thread { .. } => parent
.relative()
.child(self.render_active_thread_or_empty_state(window, cx))
.children(self.render_tool_use_limit_reached(cx))
.child(h_flex().child(self.message_editor.clone()))
.children(self.render_last_error(cx))
.child(self.render_drag_target(cx)),
ActiveView::History => parent.child(self.history.clone()),
ActiveView::PromptEditor {
context_editor,
buffer_search_bar,
..
} => {
let mut registrar = buffer_search::DivRegistrar::new(
|this, _, _cx| match &this.active_view {
ActiveView::PromptEditor {
buffer_search_bar, ..
} => Some(buffer_search_bar.clone()),
_ => None,
},
cx,
);
BufferSearchBar::register(&mut registrar);
parent.child(
registrar
.into_div()
.size_full()
.relative()
.map(|parent| {
buffer_search_bar.update(cx, |buffer_search_bar, cx| {
if buffer_search_bar.is_dismissed() {
return parent;
}
parent.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.bg(cx.theme().colors().editor_background)
.child(buffer_search_bar.render(window, cx)),
)
})
})
.child(context_editor.clone())
.child(self.render_drag_target(cx)),
)
}
} => parent.child(self.render_prompt_editor(
context_editor,
buffer_search_bar,
window,
cx,
)),
ActiveView::Configuration => parent.children(self.configuration.clone()),
})
});
match self.active_view.which_font_size_used() {
WhichFontSize::AgentFont => {
WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
.size_full()
.child(content)
.into_any()
}
_ => content.into_any(),
}
}
}
@@ -2810,28 +2857,26 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
})
}
fn focus_assistant_panel(
fn focus_agent_panel(
&self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> bool {
workspace
.focus_panel::<AssistantPanel>(window, cx)
.is_some()
workspace.focus_panel::<AgentPanel>(window, cx).is_some()
}
}
pub struct ConcreteAssistantPanelDelegate;
impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
_window: &mut Window,
cx: &mut Context<Workspace>,
) -> Option<Entity<ContextEditor>> {
let panel = workspace.panel::<AssistantPanel>(cx)?;
let panel = workspace.panel::<AgentPanel>(cx)?;
panel.read(cx).active_context_editor()
}
@@ -2842,7 +2887,7 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Task<Result<()>> {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
return Task::ready(Err(anyhow!("Agent panel not found")));
};
@@ -2869,12 +2914,12 @@ impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
return;
};
if !panel.focus_handle(cx).contains_focused(window, cx) {
workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
workspace.toggle_panel_focus::<AgentPanel>(window, cx);
}
panel.update(cx, |_, cx| {

View File

@@ -1,135 +0,0 @@
mod active_thread;
mod agent_diff;
mod assistant_configuration;
mod assistant_model_selector;
mod assistant_panel;
mod buffer_codegen;
mod context;
mod context_picker;
mod context_server_configuration;
mod context_server_tool;
mod context_store;
mod context_strip;
mod debug;
mod history_store;
mod inline_assistant;
mod inline_prompt_editor;
mod message_editor;
mod profile_selector;
mod terminal_codegen;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod tool_compatibility;
mod tool_use;
mod ui;
use std::sync::Arc;
use assistant_settings::{AgentProfileId, AssistantSettings};
use client::Client;
use fs::Fs;
use gpui::{App, actions, impl_actions};
use language::LanguageRegistry;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
use settings::Settings as _;
use thread::ThreadId;
pub use crate::active_thread::ActiveThread;
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
pub use crate::thread::{Message, MessageSegment, Thread, ThreadEvent};
pub use crate::thread_store::{TextThreadStore, ThreadStore};
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use context_store::ContextStore;
pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
agent,
[
NewTextThread,
ToggleContextPicker,
ToggleNavigationMenu,
ToggleOptionsMenu,
DeleteRecentlyOpenThread,
ToggleProfileSelector,
RemoveAllContext,
ExpandMessageEditor,
OpenHistory,
AddContextServer,
RemoveSelectedThread,
Chat,
CycleNextInlineAssist,
CyclePreviousInlineAssist,
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
RemoveFocusedContext,
AcceptSuggestedContext,
OpenActiveThreadAsMarkdown,
OpenAgentDiff,
Keep,
Reject,
RejectAll,
KeepAll,
Follow,
ResetTrialUpsell,
]
);
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
}
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
}
impl ManageProfiles {
pub fn customize_tools(profile_id: AgentProfileId) -> Self {
Self {
customize_tools: Some(profile_id),
}
}
}
impl_actions!(agent, [NewThread, ManageProfiles]);
/// Initializes the `agent` crate.
pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
AssistantSettings::register(cx);
thread_store::init(cx);
assistant_panel::init(cx);
context_server_configuration::init(language_registry, cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
cx.observe_new(AddContextServerModal::register).detach();
cx.observe_new(ManageProfilesModal::register).detach();
}

View File

@@ -466,6 +466,7 @@ impl CodegenAlternative {
prompt_id: None,
mode: None,
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature,
messages: vec![request_message],

View File

@@ -754,11 +754,11 @@ pub enum ImageStatus {
impl ImageContext {
pub fn eq_for_key(&self, other: &Self) -> bool {
self.original_image.id == other.original_image.id
self.original_image.id() == other.original_image.id()
}
pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
self.original_image.id.hash(state);
self.original_image.id().hash(state);
}
pub fn image(&self) -> Option<LanguageModelImage> {

View File

@@ -36,7 +36,7 @@ use ui::{
use uuid::Uuid;
use workspace::{Workspace, notifications::NotifyResultExt};
use crate::AssistantPanel;
use crate::AgentPanel;
use crate::context::RULES_ICON;
use crate::context_store::ContextStore;
use crate::thread::ThreadId;
@@ -425,9 +425,9 @@ impl ContextPicker {
render_thread_context_entry(&view_thread, context_store.clone(), cx)
.into_any()
},
move |_window, cx| {
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.add_recent_thread(thread.clone(), cx)
this.add_recent_thread(thread.clone(), window, cx)
.detach_and_log_err(cx);
})
},
@@ -459,6 +459,7 @@ impl ContextPicker {
fn add_recent_thread(
&self,
entry: ThreadContextEntry,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let Some(context_store) = self.context_store.upgrade() else {
@@ -476,7 +477,7 @@ impl ContextPicker {
};
let open_thread_task =
thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;
context_store.update(cx, |context_store, cx| {
@@ -647,7 +648,7 @@ fn recent_context_picker_entries(
let current_threads = context_store.read(cx).thread_ids();
let active_thread_id = workspace
.panel::<AssistantPanel>(cx)
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
if let Some((thread_store, text_thread_store)) = thread_store

View File

@@ -438,15 +438,15 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |cx| match &thread_entry {
move |window, cx| match &thread_entry {
ThreadContextEntry::Thread { id, .. } => {
let thread_id = id.clone();
let context_store = context_store.clone();
let thread_store = thread_store.clone();
cx.spawn::<_, Option<_>>(async move |cx| {
window.spawn::<_, Option<_>>(cx, async move |cx| {
let thread: Entity<Thread> = thread_store
.update(cx, |thread_store, cx| {
thread_store.open_thread(&thread_id, cx)
.update_in(cx, |thread_store, window, cx| {
thread_store.open_thread(&thread_id, window, cx)
})
.ok()?
.await
@@ -507,7 +507,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |cx| {
move |_, cx| {
let user_prompt_id = rules.prompt_id;
let context = context_store.update(cx, |context_store, cx| {
context_store.add_rules(user_prompt_id, false, cx)
@@ -544,7 +544,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |cx| {
move |_, cx| {
let context_store = context_store.clone();
let http_client = http_client.clone();
let url_to_fetch = url_to_fetch.clone();
@@ -629,7 +629,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor,
context_store.clone(),
move |cx| {
move |_, cx| {
if is_directory {
Task::ready(
context_store
@@ -700,7 +700,7 @@ impl ContextPickerCompletionProvider {
new_text_len,
editor.clone(),
context_store.clone(),
move |cx| {
move |_, cx| {
let symbol = symbol.clone();
let context_store = context_store.clone();
let workspace = workspace.clone();
@@ -954,10 +954,13 @@ fn confirm_completion_callback(
content_len: usize,
editor: Entity<Editor>,
context_store: Entity<ContextStore>,
add_context_fn: impl Fn(&mut App) -> Task<Option<AgentContextHandle>> + Send + Sync + 'static,
add_context_fn: impl Fn(&mut Window, &mut App) -> Task<Option<AgentContextHandle>>
+ Send
+ Sync
+ 'static,
) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
Arc::new(move |_, window, cx| {
let context = add_context_fn(cx);
let context = add_context_fn(window, cx);
let crease_text = crease_text.clone();
let crease_icon_path = crease_icon_path.clone();

View File

@@ -154,7 +154,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
})
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
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;
};
@@ -165,7 +165,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
return;
};
let open_thread_task =
thread_store.update(cx, |this, cx| this.open_thread(&id, cx));
thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx));
cx.spawn(async move |this, cx| {
let thread = open_thread_task.await?;

View File

@@ -10,7 +10,7 @@ use ui::prelude::*;
use util::ResultExt;
use workspace::Workspace;
use crate::assistant_configuration::ConfigureContextServerModal;
use crate::agent_configuration::ConfigureContextServerModal;
pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
cx.observe_new(move |_: &mut Workspace, window, cx| {

View File

@@ -4,7 +4,7 @@ use anyhow::{Result, anyhow, bail};
use assistant_tool::{ActionLog, Tool, ToolResult, ToolSource};
use context_server::{ContextServerId, types};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, context_server_store::ContextServerStore};
use ui::IconName;
@@ -72,9 +72,10 @@ impl Tool for ContextServerTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -115,7 +116,7 @@ impl Tool for ContextServerTool {
}
}
}
Ok(result)
Ok(result.into())
})
.into()
} else {

View File

@@ -22,7 +22,7 @@ use crate::thread::Thread;
use crate::thread_store::{TextThreadStore, ThreadStore};
use crate::ui::{AddedContext, ContextPill};
use crate::{
AcceptSuggestedContext, AssistantPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
};
@@ -144,7 +144,7 @@ impl ContextStrip {
}
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AssistantPanel>(cx)?.read(cx);
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
if let Some(active_thread) = panel.active_thread() {
let weak_active_thread = active_thread.downgrade();

View File

@@ -8,7 +8,7 @@ use gpui::{Entity, Task, prelude::*};
use serde::{Deserialize, Serialize};
use smol::future::FutureExt;
use std::time::Duration;
use ui::{App, SharedString};
use ui::{App, SharedString, Window};
use util::ResultExt as _;
use crate::{
@@ -82,6 +82,7 @@ impl HistoryStore {
thread_store: Entity<ThreadStore>,
context_store: Entity<assistant_context_editor::ContextStore>,
initial_recent_entries: impl IntoIterator<Item = RecentEntry>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let subscriptions = vec![
@@ -89,56 +90,62 @@ impl HistoryStore {
cx.observe(&context_store, |_, _, cx| cx.notify()),
];
cx.spawn({
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
async move |this, cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update(cx, |thread_store, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| async { Err(anyhow!("no thread store")) }.boxed()),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| async { Err(anyhow!("no context store")) }.boxed()),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect::<VecDeque<_>>();
window
.spawn(cx, {
let thread_store = thread_store.downgrade();
let context_store = context_store.downgrade();
let this = cx.weak_entity();
async move |cx| {
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
let contents = cx
.background_spawn(async move { std::fs::read_to_string(path) })
.await
.ok()?;
let entries = serde_json::from_str::<Vec<SerializedRecentEntry>>(&contents)
.context("deserializing persisted agent panel navigation history")
.log_err()?
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.map(|serialized| match serialized {
SerializedRecentEntry::Thread(id) => thread_store
.update_in(cx, |thread_store, window, cx| {
let thread_id = ThreadId::from(id.as_str());
thread_store
.open_thread(&thread_id, window, cx)
.map_ok(|thread| RecentEntry::Thread(thread_id, thread))
.boxed()
})
.unwrap_or_else(|_| {
async { Err(anyhow!("no thread store")) }.boxed()
}),
SerializedRecentEntry::Context(id) => context_store
.update(cx, |context_store, cx| {
context_store
.open_local_context(Path::new(&id).into(), cx)
.map_ok(RecentEntry::Context)
.boxed()
})
.unwrap_or_else(|_| {
async { Err(anyhow!("no context store")) }.boxed()
}),
});
let entries = join_all(entries)
.await
.into_iter()
.filter_map(|result| result.log_err())
.collect::<VecDeque<_>>();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
this.update(cx, |this, _| {
this.recently_opened_entries.extend(entries);
this.recently_opened_entries
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
})
.ok();
Some(())
}
})
.detach();
Some(())
}
})
.detach();
Self {
thread_store,

View File

@@ -8,9 +8,10 @@ use anyhow::{Context as _, Result};
use assistant_settings::AssistantSettings;
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::display_map::EditorMargins;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
actions::SelectAll,
display_map::{
BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock,
@@ -42,7 +43,7 @@ use util::ResultExt;
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
use crate::AssistantPanel;
use crate::AgentPanel;
use crate::buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent};
use crate::context_store::ContextStore;
use crate::inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent};
@@ -181,13 +182,12 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| {
if is_assistant2_enabled {
let panel = workspace.read(cx).panel::<AssistantPanel>(cx);
let panel = workspace.read(cx).panel::<AgentPanel>(cx);
let thread_store = panel
.as_ref()
.map(|assistant_panel| assistant_panel.read(cx).thread_store().downgrade());
let text_thread_store = panel.map(|assistant_panel| {
assistant_panel.read(cx).text_thread_store().downgrade()
});
.map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
let text_thread_store = panel
.map(|agent_panel| agent_panel.read(cx).text_thread_store().downgrade());
editor.add_code_action_provider(
Rc::new(AssistantCodeActionProvider {
@@ -226,7 +226,7 @@ impl InlineAssistant {
let Some(inline_assist_target) = Self::resolve_inline_assist_target(
workspace,
workspace.panel::<AssistantPanel>(cx),
workspace.panel::<AgentPanel>(cx),
window,
cx,
) else {
@@ -239,15 +239,15 @@ impl InlineAssistant {
.map_or(false, |model| model.provider.is_authenticated(cx))
};
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
return;
};
let assistant_panel = assistant_panel.read(cx);
let agent_panel = agent_panel.read(cx);
let prompt_store = assistant_panel.prompt_store().as_ref().cloned();
let thread_store = Some(assistant_panel.thread_store().downgrade());
let text_thread_store = Some(assistant_panel.text_thread_store().downgrade());
let context_store = assistant_panel.inline_assist_context_store().clone();
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
let thread_store = Some(agent_panel.thread_store().downgrade());
let text_thread_store = Some(agent_panel.text_thread_store().downgrade());
let context_store = agent_panel.inline_assist_context_store().clone();
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -458,11 +458,11 @@ impl InlineAssistant {
)
});
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
gutter_dimensions.clone(),
editor_margins,
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
@@ -577,11 +577,11 @@ impl InlineAssistant {
)
});
let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default()));
let editor_margins = Arc::new(Mutex::new(EditorMargins::default()));
let prompt_editor = cx.new(|cx| {
PromptEditor::new_buffer(
assist_id,
gutter_dimensions.clone(),
editor_margins,
self.prompt_history.clone(),
prompt_buffer.clone(),
codegen.clone(),
@@ -650,6 +650,7 @@ impl InlineAssistant {
height: Some(prompt_editor_height),
render: build_assist_editor_renderer(prompt_editor),
priority: 0,
render_in_minimap: false,
},
BlockProperties {
style: BlockStyle::Sticky,
@@ -664,6 +665,7 @@ impl InlineAssistant {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
},
];
@@ -1405,11 +1407,11 @@ impl InlineAssistant {
enum DeletedLines {}
let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_gutter(false, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_read_only(true);
editor.set_show_edit_predictions(Some(false), window, cx);
editor.highlight_rows::<DeletedLines>(
@@ -1433,11 +1435,12 @@ impl InlineAssistant {
.bg(cx.theme().status().deleted_background)
.size_full()
.h(height as f32 * cx.window.line_height())
.pl(cx.gutter_dimensions.full_width())
.pl(cx.margins.gutter.full_width())
.child(deleted_lines_editor.clone())
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
});
}
@@ -1450,7 +1453,7 @@ impl InlineAssistant {
fn resolve_inline_assist_target(
workspace: &mut Workspace,
assistant_panel: Option<Entity<AssistantPanel>>,
agent_panel: Option<Entity<AgentPanel>>,
window: &mut Window,
cx: &mut App,
) -> Option<InlineAssistTarget> {
@@ -1470,7 +1473,7 @@ impl InlineAssistant {
}
}
let context_editor = assistant_panel
let context_editor = agent_panel
.and_then(|panel| panel.read(cx).active_context_editor())
.and_then(|editor| {
let editor = &editor.read(cx).editor().clone();
@@ -1595,9 +1598,9 @@ fn build_assist_editor_renderer(editor: &Entity<PromptEditor<BufferCodegen>>) ->
let editor = editor.clone();
Arc::new(move |cx: &mut BlockContext| {
let gutter_dimensions = editor.read(cx).gutter_dimensions();
let editor_margins = editor.read(cx).editor_margins();
*gutter_dimensions.lock() = *cx.gutter_dimensions;
*editor_margins.lock() = *cx.margins;
editor.clone().into_any_element()
})
}

View File

@@ -1,4 +1,4 @@
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::buffer_codegen::BufferCodegen;
use crate::context::ContextCreasesAddon;
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
@@ -11,9 +11,9 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
use crate::{RemoveAllContext, ToggleContextPicker};
use client::ErrorExt;
use collections::VecDeque;
use editor::display_map::EditorMargins;
use editor::{
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle,
GutterDimensions, MultiBuffer,
ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
actions::{MoveDown, MoveUp},
};
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
@@ -42,7 +42,7 @@ pub struct PromptEditor<T> {
context_store: Entity<ContextStore>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
model_selector: Entity<AgentModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -61,11 +61,13 @@ impl<T: 'static> Render for PromptEditor<T> {
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
let mut buttons = Vec::new();
let left_gutter_width = match &self.mode {
const RIGHT_PADDING: Pixels = px(9.);
let (left_gutter_width, right_padding) = match &self.mode {
PromptEditorMode::Buffer {
id: _,
codegen,
gutter_dimensions,
editor_margins,
} => {
let codegen = codegen.read(cx);
@@ -73,13 +75,17 @@ impl<T: 'static> Render for PromptEditor<T> {
buttons.push(self.render_cycle_controls(&codegen, cx));
}
let gutter_dimensions = gutter_dimensions.lock();
let editor_margins = editor_margins.lock();
let gutter = editor_margins.gutter;
gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)
let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
let right_padding = editor_margins.right + RIGHT_PADDING;
(left_gutter_width, right_padding)
}
PromptEditorMode::Terminal { .. } => {
// Give the equivalent of the same left-padding that we're using on the right
Pixels::from(40.0)
(Pixels::from(40.0), Pixels::from(24.))
}
};
@@ -100,7 +106,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.size_full()
.pt_0p5()
.pb(bottom_padding)
.pr_6()
.pr(right_padding)
.child(
h_flex()
.items_start()
@@ -284,12 +290,12 @@ impl<T: 'static> PromptEditor<T> {
PromptEditorMode::Terminal { .. } => "Generate",
};
let assistant_panel_keybinding =
let agent_panel_keybinding =
ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
.map(|keybinding| format!("{keybinding} to chat ― "))
.unwrap_or_default();
format!("{action}… ({assistant_panel_keybinding}↓↑ for history)")
format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
}
pub fn prompt(&self, cx: &App) -> String {
@@ -806,7 +812,7 @@ pub enum PromptEditorMode {
Buffer {
id: InlineAssistId,
codegen: Entity<BufferCodegen>,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
editor_margins: Arc<Mutex<EditorMargins>>,
},
Terminal {
id: TerminalInlineAssistId,
@@ -838,7 +844,7 @@ impl InlineAssistId {
impl PromptEditor<BufferCodegen> {
pub fn new_buffer(
id: InlineAssistId,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
editor_margins: Arc<Mutex<EditorMargins>>,
prompt_history: VecDeque<String>,
prompt_buffer: Entity<MultiBuffer>,
codegen: Entity<BufferCodegen>,
@@ -855,7 +861,7 @@ impl PromptEditor<BufferCodegen> {
let mode = PromptEditorMode::Buffer {
id,
codegen,
gutter_dimensions,
editor_margins,
};
let prompt_editor = cx.new(|cx| {
@@ -921,7 +927,7 @@ impl PromptEditor<BufferCodegen> {
context_strip,
context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
AgentModelSelector::new(
fs,
model_selector_menu_handle,
prompt_editor.focus_handle(cx),
@@ -995,11 +1001,9 @@ impl PromptEditor<BufferCodegen> {
}
}
pub fn gutter_dimensions(&self) -> &Arc<Mutex<GutterDimensions>> {
pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
match &self.mode {
PromptEditorMode::Buffer {
gutter_dimensions, ..
} => gutter_dimensions,
PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
PromptEditorMode::Terminal { .. } => unreachable!(),
}
}
@@ -1094,7 +1098,7 @@ impl PromptEditor<TerminalCodegen> {
context_strip,
context_picker_menu_handle,
model_selector: cx.new(|cx| {
AssistantModelSelector::new(
AgentModelSelector::new(
fs,
model_selector_menu_handle.clone(),
prompt_editor.focus_handle(cx),

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::assistant_model_selector::{AssistantModelSelector, ModelType};
use crate::agent_model_selector::{AgentModelSelector, ModelType};
use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
use crate::ui::{
@@ -38,9 +38,8 @@ use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
use ui::{Disclosure, DocumentationSide, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
use util::{ResultExt as _, maybe};
use workspace::dock::DockPosition;
use workspace::{CollaboratorId, Workspace};
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
@@ -66,7 +65,7 @@ pub struct MessageEditor {
prompt_store: Option<Entity<PromptStore>>,
context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AssistantModelSelector>,
model_selector: Entity<AgentModelSelector>,
last_loaded_context: Option<ContextLoadResult>,
load_context_task: Option<Shared<Task<()>>>,
profile_selector: Entity<ProfileSelector>,
@@ -133,14 +132,6 @@ pub(crate) fn create_editor(
editor
}
fn documentation_side(position: DockPosition) -> DocumentationSide {
match position {
DockPosition::Left => DocumentationSide::Right,
DockPosition::Bottom => DocumentationSide::Left,
DockPosition::Right => DocumentationSide::Left,
}
}
impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
@@ -151,7 +142,6 @@ impl MessageEditor {
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>,
thread: Entity<Thread>,
dock_position: DockPosition,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -199,7 +189,7 @@ impl MessageEditor {
];
let model_selector = cx.new(|cx| {
AssistantModelSelector::new(
AgentModelSelector::new(
fs.clone(),
model_selector_menu_handle,
editor.focus_handle(cx),
@@ -209,6 +199,10 @@ impl MessageEditor {
)
});
let profile_selector = cx.new(|cx| {
ProfileSelector::new(thread.clone(), thread_store, editor.focus_handle(cx), cx)
});
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
@@ -225,15 +219,7 @@ impl MessageEditor {
model_selector,
edits_expanded: false,
editor_is_expanded: false,
profile_selector: cx.new(|cx| {
ProfileSelector::new(
fs,
thread_store,
editor.focus_handle(cx),
documentation_side(dock_position),
cx,
)
}),
profile_selector,
last_estimated_token_count: None,
update_token_count_task: None,
_subscriptions: subscriptions,
@@ -637,7 +623,7 @@ impl MessageEditor {
this.h(vh(0.8, window)).justify_between()
})
.child(
div()
v_flex()
.min_h_16()
.when(is_editor_expanded, |this| this.h_full())
.child({
@@ -1259,6 +1245,7 @@ impl MessageEditor {
mode: None,
messages: vec![request_message],
tools: vec![],
tool_choice: None,
stop: vec![],
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
};
@@ -1283,12 +1270,6 @@ impl MessageEditor {
.ok();
}));
}
pub fn set_dock_position(&mut self, position: DockPosition, cx: &mut Context<Self>) {
self.profile_selector.update(cx, |profile_selector, cx| {
profile_selector.set_documentation_side(documentation_side(position), cx)
});
}
}
pub fn extract_message_creases(
@@ -1462,7 +1443,6 @@ impl AgentPreview for MessageEditor {
thread_store.downgrade(),
text_thread_store.downgrade(),
thread,
DockPosition::Left,
window,
cx,
)

View File

@@ -1,36 +1,32 @@
use std::sync::Arc;
use assistant_settings::{
AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
AgentProfile, AgentProfileId, AssistantDockPosition, AssistantSettings, GroupedAgentProfiles,
builtin_profiles,
};
use fs::Fs;
use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
use language_model::LanguageModelRegistry;
use settings::{Settings as _, SettingsStore, update_settings_file};
use settings::{Settings as _, SettingsStore};
use ui::{
ContextMenu, ContextMenuEntry, DocumentationSide, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use util::ResultExt as _;
use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
use crate::{ManageProfiles, Thread, ThreadStore, ToggleProfileSelector};
pub struct ProfileSelector {
profiles: GroupedAgentProfiles,
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
_subscriptions: Vec<Subscription>,
documentation_side: DocumentationSide,
}
impl ProfileSelector {
pub fn new(
fs: Arc<dyn Fs>,
thread: Entity<Thread>,
thread_store: WeakEntity<ThreadStore>,
focus_handle: FocusHandle,
documentation_side: DocumentationSide,
cx: &mut Context<Self>,
) -> Self {
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
@@ -39,20 +35,14 @@ impl ProfileSelector {
Self {
profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
fs,
thread,
thread_store,
menu_handle: PopoverMenuHandle::default(),
focus_handle,
_subscriptions: vec![settings_subscription],
documentation_side,
}
}
pub fn set_documentation_side(&mut self, side: DocumentationSide, cx: &mut Context<Self>) {
self.documentation_side = side;
cx.notify();
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
@@ -69,8 +59,12 @@ impl ProfileSelector {
ContextMenu::build(window, cx, |mut menu, _window, cx| {
let settings = AssistantSettings::get_global(cx);
for (profile_id, profile) in self.profiles.builtin.iter() {
menu =
menu.item(self.menu_entry_for_profile(profile_id.clone(), profile, settings));
menu = menu.item(self.menu_entry_for_profile(
profile_id.clone(),
profile,
settings,
cx,
));
}
if !self.profiles.custom.is_empty() {
@@ -80,6 +74,7 @@ impl ProfileSelector {
profile_id.clone(),
profile,
settings,
cx,
));
}
}
@@ -100,6 +95,7 @@ impl ProfileSelector {
profile_id: AgentProfileId,
profile: &AgentProfile,
settings: &AssistantSettings,
cx: &App,
) -> ContextMenuEntry {
let documentation = match profile.name.to_lowercase().as_str() {
builtin_profiles::WRITE => Some("Get help to write anything."),
@@ -108,11 +104,15 @@ impl ProfileSelector {
_ => None,
};
let entry = ContextMenuEntry::new(profile.name.clone())
.toggleable(IconPosition::End, profile_id == settings.default_profile);
let current_profile_id = self.thread.read(cx).configured_profile_id();
let entry = ContextMenuEntry::new(profile.name.clone()).toggleable(
IconPosition::End,
Some(profile_id.clone()) == current_profile_id,
);
let entry = if let Some(doc_text) = documentation {
entry.documentation_aside(self.documentation_side, move |_| {
entry.documentation_aside(documentation_side(settings.dock), move |_| {
Label::new(doc_text).into_any_element()
})
} else {
@@ -120,15 +120,13 @@ impl ProfileSelector {
};
entry.handler({
let fs = self.fs.clone();
let thread_store = self.thread_store.clone();
let profile_id = profile_id.clone();
let thread = self.thread.clone();
move |_window, cx| {
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
let profile_id = profile_id.clone();
move |settings, _cx| {
settings.set_profile(profile_id.clone());
}
thread.update(cx, |thread, cx| {
thread.set_configured_profile_id(Some(profile_id.clone()), cx);
});
thread_store
@@ -144,58 +142,78 @@ impl ProfileSelector {
impl Render for ProfileSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AssistantSettings::get_global(cx);
let profile_id = &settings.default_profile;
let profile = settings.profiles.get(profile_id);
let profile_id = self
.thread
.read(cx)
.configured_profile_id()
.unwrap_or(settings.default_profile.clone());
let profile = settings.profiles.get(&profile_id).cloned();
let selected_profile = profile
.map(|profile| profile.name.clone())
.unwrap_or_else(|| "Unknown".into());
let model_registry = LanguageModelRegistry::read_global(cx);
let supports_tools = model_registry
.default_model()
.map_or(false, |default| default.model.supports_tools());
let configured_model = self
.thread
.read_with(cx, |thread, _cx| thread.configured_model())
.or_else(|| {
let model_registry = LanguageModelRegistry::read_global(cx);
model_registry.default_model()
});
let supports_tools =
configured_model.map_or(false, |default| default.model.supports_tools());
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
let trigger_button = if supports_tools {
Button::new("profile-selector-model", selected_profile)
if supports_tools {
let this = cx.entity().clone();
let focus_handle = self.focus_handle.clone();
let trigger_button = Button::new("profile-selector-model", selected_profile)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.icon_color(Color::Muted);
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
}
})
.anchor(
if documentation_side(settings.dock) == DocumentationSide::Left {
gpui::Corner::BottomRight
} else {
gpui::Corner::BottomLeft
},
)
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.into_any_element()
} else {
Button::new("tools-not-supported-button", "No Tools")
Button::new("tools-not-supported-button", "Tools Unsupported")
.disabled(true)
.label_size(LabelSize::Small)
.color(Color::Muted)
.tooltip(Tooltip::text("The current model does not support tools."))
};
PopoverMenu::new("profile-selector")
.trigger_with_tooltip(trigger_button, {
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Toggle Profile Menu",
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)
}
})
.anchor(if self.documentation_side == DocumentationSide::Left {
gpui::Corner::BottomRight
} else {
gpui::Corner::BottomLeft
})
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
.tooltip(Tooltip::text("This model does not support tools."))
.into_any_element()
}
}
}
fn documentation_side(position: AssistantDockPosition) -> DocumentationSide {
match position {
AssistantDockPosition::Left => DocumentationSide::Right,
AssistantDockPosition::Bottom => DocumentationSide::Left,
AssistantDockPosition::Right => DocumentationSide::Left,
}
}

View File

@@ -293,6 +293,7 @@ impl TerminalInlineAssistant {
mode: None,
messages: vec![request_message],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature,
}

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use std::time::Instant;
use anyhow::{Result, anyhow};
use assistant_settings::{AssistantSettings, CompletionMode};
use assistant_settings::{AgentProfileId, AssistantSettings, CompletionMode};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
@@ -35,6 +35,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use thiserror::Error;
use ui::Window;
use util::{ResultExt as _, TryFutureExt as _, post_inc};
use uuid::Uuid;
use zed_llm_client::CompletionRequestStatus;
@@ -358,6 +359,7 @@ pub struct Thread {
>,
remaining_turns: u32,
configured_model: Option<ConfiguredModel>,
configured_profile_id: Option<AgentProfileId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -378,6 +380,8 @@ impl Thread {
) -> Self {
let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel();
let configured_model = LanguageModelRegistry::read_global(cx).default_model();
let assistant_settings = AssistantSettings::get_global(cx);
let configured_profile_id = assistant_settings.default_profile.clone();
Self {
id: ThreadId::new(),
@@ -420,6 +424,7 @@ impl Thread {
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
configured_profile_id: Some(configured_profile_id),
}
}
@@ -430,6 +435,7 @@ impl Thread {
tools: Entity<ToolWorkingSet>,
prompt_builder: Arc<PromptBuilder>,
project_context: SharedProjectContext,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let next_message_id = MessageId(
@@ -439,7 +445,13 @@ impl Thread {
.map(|message| message.id.0 + 1)
.unwrap_or(0),
);
let tool_use = ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages);
let tool_use = ToolUseState::from_serialized_messages(
tools.clone(),
&serialized.messages,
project.clone(),
window,
cx,
);
let (detailed_summary_tx, detailed_summary_rx) =
postage::watch::channel_with(serialized.detailed_summary_state);
@@ -460,6 +472,8 @@ impl Thread {
.completion_mode
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
let configured_profile_id = serialized.profile.clone();
Self {
id,
updated_at: serialized.updated_at,
@@ -533,6 +547,7 @@ impl Thread {
request_callback: None,
remaining_turns: u32::MAX,
configured_model,
configured_profile_id,
}
}
@@ -588,6 +603,19 @@ impl Thread {
cx.notify();
}
pub fn configured_profile_id(&self) -> Option<AgentProfileId> {
self.configured_profile_id.clone()
}
pub fn set_configured_profile_id(
&mut self,
id: Option<AgentProfileId>,
cx: &mut Context<Self>,
) {
self.configured_profile_id = id;
cx.notify();
}
pub const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
pub fn summary_or_default(&self) -> SharedString {
@@ -1064,6 +1092,7 @@ impl Thread {
tool_use_id: tool_result.tool_use_id.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
output: tool_result.output.clone(),
})
.collect(),
context: message.loaded_context.text.clone(),
@@ -1091,6 +1120,7 @@ impl Thread {
provider: model.provider.id().0.to_string(),
model: model.model.id().0.to_string(),
}),
profile: this.configured_profile_id.clone(),
completion_mode: Some(this.completion_mode),
})
})
@@ -1144,6 +1174,7 @@ impl Thread {
mode: None,
messages: vec![],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(&model, cx),
};
@@ -1188,6 +1219,7 @@ impl Thread {
}));
}
let mut message_ix_to_cache = None;
for message in &self.messages {
let mut request_message = LanguageModelRequestMessage {
role: message.role,
@@ -1224,19 +1256,57 @@ impl Thread {
};
}
self.tool_use
.attach_tool_uses(message.id, &mut request_message);
let mut cache_message = true;
let mut tool_results_message = LanguageModelRequestMessage {
role: Role::User,
content: Vec::new(),
cache: false,
};
for (tool_use, tool_result) in self.tool_use.tool_results(message.id) {
if let Some(tool_result) = tool_result {
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
tool_results_message
.content
.push(MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_use.id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error,
content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
"<Tool returned an empty string>".into()
} else {
tool_result.content.clone()
},
output: None,
}));
} else {
cache_message = false;
log::debug!(
"skipped tool use {:?} because it is still pending",
tool_use
);
}
}
if cache_message {
message_ix_to_cache = Some(request.messages.len());
}
request.messages.push(request_message);
if let Some(tool_results_message) = self.tool_use.tool_results_message(message.id) {
if !tool_results_message.content.is_empty() {
if cache_message {
message_ix_to_cache = Some(request.messages.len());
}
request.messages.push(tool_results_message);
}
}
// https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
if let Some(last) = request.messages.last_mut() {
last.cache = true;
if let Some(message_ix_to_cache) = message_ix_to_cache {
request.messages[message_ix_to_cache].cache = true;
}
self.attached_tracked_files_state(&mut request.messages, cx);
@@ -1263,6 +1333,7 @@ impl Thread {
mode: None,
messages: vec![],
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: AssistantSettings::temperature_for_model(model, cx),
};
@@ -1537,9 +1608,9 @@ impl Thread {
completion.queue_state = QueueState::Started;
}
CompletionRequestStatus::Failed {
code, message
code, message, request_id
} => {
return Err(anyhow!("completion request failed. code: {code}, message: {message}"));
return Err(anyhow!("completion request failed. request_id: {request_id}, code: {code}, message: {message}"));
}
CompletionRequestStatus::UsageUpdated {
amount, limit
@@ -1879,8 +1950,7 @@ impl Thread {
model: Arc<dyn LanguageModel>,
) -> Vec<PendingToolUse> {
self.auto_capture_telemetry(cx);
let request = self.to_completion_request(model, cx);
let messages = Arc::new(request.messages);
let request = Arc::new(self.to_completion_request(model.clone(), cx));
let pending_tool_uses = self
.tool_use
.pending_tool_uses()
@@ -1898,7 +1968,7 @@ impl Thread {
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
messages.clone(),
request.clone(),
tool,
);
cx.emit(ThreadEvent::ToolConfirmationNeeded);
@@ -1907,8 +1977,9 @@ impl Thread {
tool_use.id.clone(),
tool_use.ui_text.clone(),
tool_use.input.clone(),
&messages,
request.clone(),
tool,
model.clone(),
window,
cx,
);
@@ -2001,12 +2072,14 @@ impl Thread {
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<SharedString>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
request: Arc<LanguageModelRequest>,
tool: Arc<dyn Tool>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) {
let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, window, cx);
let task =
self.spawn_tool_use(tool_use_id.clone(), request, input, tool, model, window, cx);
self.tool_use
.run_pending_tool(tool_use_id, ui_text.into(), task);
}
@@ -2014,9 +2087,10 @@ impl Thread {
fn spawn_tool_use(
&mut self,
tool_use_id: LanguageModelToolUseId,
messages: &[LanguageModelRequestMessage],
request: Arc<LanguageModelRequest>,
input: serde_json::Value,
tool: Arc<dyn Tool>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut Context<Thread>,
) -> Task<()> {
@@ -2027,9 +2101,10 @@ impl Thread {
} else {
tool.run(
input,
messages,
request,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
)
@@ -2309,7 +2384,7 @@ impl Thread {
.map(|repo| {
repo.update(cx, |repo, _| {
let current_branch =
repo.branch.as_ref().map(|branch| branch.name.to_string());
repo.branch.as_ref().map(|branch| branch.name().to_owned());
repo.send_job(None, |state, _| async move {
let RepositoryState::Local { backend, .. } = state else {
return GitState {
@@ -2412,6 +2487,13 @@ impl Thread {
writeln!(markdown, "**\n")?;
writeln!(markdown, "{}", tool_result.content)?;
if let Some(output) = tool_result.output.as_ref() {
writeln!(
markdown,
"\n\nDebug Output:\n\n```json\n{}\n```\n",
serde_json::to_string_pretty(output)?
)?;
}
}
}

View File

@@ -3,7 +3,7 @@ use std::ops::Range;
use std::sync::Arc;
use assistant_context_editor::SavedContextMetadata;
use chrono::{Datelike as _, NaiveDate, TimeDelta, Utc};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use editor::{Editor, EditorEvent};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -19,10 +19,10 @@ use util::ResultExt;
use crate::history_store::{HistoryEntry, HistoryStore};
use crate::thread_store::SerializedThreadMetadata;
use crate::{AssistantPanel, RemoveSelectedThread};
use crate::{AgentPanel, RemoveSelectedThread};
pub struct ThreadHistory {
assistant_panel: WeakEntity<AssistantPanel>,
agent_panel: WeakEntity<AgentPanel>,
history_store: Entity<HistoryStore>,
scroll_handle: UniformListScrollHandle,
selected_index: usize,
@@ -31,6 +31,8 @@ pub struct ThreadHistory {
// When the search is empty, we display date separators between history entries
// This vector contains an enum of either a separator or an actual entry
separated_items: Vec<HistoryListItem>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool,
@@ -69,7 +71,7 @@ impl HistoryListItem {
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakEntity<AssistantPanel>,
agent_panel: WeakEntity<AgentPanel>,
history_store: Entity<HistoryStore>,
window: &mut Window,
cx: &mut Context<Self>,
@@ -96,13 +98,14 @@ impl ThreadHistory {
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
assistant_panel,
agent_panel,
history_store,
scroll_handle,
selected_index: 0,
search_state: SearchState::Empty,
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor,
scrollbar_visibility: true,
scrollbar_state,
@@ -119,7 +122,7 @@ impl ThreadHistory {
.update(cx, |store, cx| store.entries(cx))
.into();
self.set_selected_index(0, cx);
self.set_selected_entry_index(0, cx);
self.update_separated_items(cx);
match &self.search_state {
@@ -133,35 +136,47 @@ impl ThreadHistory {
fn update_separated_items(&mut self, cx: &mut Context<Self>) {
self._separated_items_task.take();
let mut separated_items = std::mem::take(&mut self.separated_items);
separated_items.clear();
let all_entries = self.all_entries.clone();
let mut items = std::mem::take(&mut self.separated_items);
let mut indexes = std::mem::take(&mut self.separated_item_indexes);
items.clear();
indexes.clear();
// We know there's going to be at least one bucket separator
items.reserve(all_entries.len() + 1);
indexes.reserve(all_entries.len() + 1);
let bg_task = cx.background_spawn(async move {
let mut bucket = None;
let today = Utc::now().naive_local().date();
let today = Local::now().naive_local().date();
for (index, entry) in all_entries.iter().enumerate() {
let entry_date = entry.updated_at().naive_local().date();
let entry_date = entry
.updated_at()
.with_timezone(&Local)
.naive_local()
.date();
let entry_bucket = TimeBucket::from_dates(today, entry_date);
if Some(entry_bucket) != bucket {
bucket = Some(entry_bucket);
separated_items.push(HistoryListItem::BucketSeparator(entry_bucket));
items.push(HistoryListItem::BucketSeparator(entry_bucket));
}
separated_items.push(HistoryListItem::Entry {
indexes.push(items.len() as u32);
items.push(HistoryListItem::Entry {
index,
format: entry_bucket.into(),
});
}
separated_items
(items, indexes)
});
let task = cx.spawn(async move |this, cx| {
let separated_items = bg_task.await;
let (items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
this.separated_items = separated_items;
this.separated_items = items;
this.separated_item_indexes = indexes;
cx.notify();
})
.log_err();
@@ -229,7 +244,7 @@ impl ThreadHistory {
matches,
};
this.set_selected_index(0, cx);
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
@@ -252,6 +267,14 @@ impl ThreadHistory {
}
}
fn list_item_count(&self) -> usize {
match &self.search_state {
SearchState::Empty => self.separated_items.len(),
SearchState::Searching { .. } => 0,
SearchState::Searched { matches, .. } => matches.len(),
}
}
fn search_produced_no_matches(&self) -> bool {
match &self.search_state {
SearchState::Empty => false,
@@ -279,9 +302,9 @@ impl ThreadHistory {
let count = self.matched_count();
if count > 0 {
if self.selected_index == 0 {
self.set_selected_index(count - 1, cx);
self.set_selected_entry_index(count - 1, cx);
} else {
self.set_selected_index(self.selected_index - 1, cx);
self.set_selected_entry_index(self.selected_index - 1, cx);
}
}
}
@@ -295,9 +318,9 @@ impl ThreadHistory {
let count = self.matched_count();
if count > 0 {
if self.selected_index == count - 1 {
self.set_selected_index(0, cx);
self.set_selected_entry_index(0, cx);
} else {
self.set_selected_index(self.selected_index + 1, cx);
self.set_selected_entry_index(self.selected_index + 1, cx);
}
}
}
@@ -310,21 +333,32 @@ impl ThreadHistory {
) {
let count = self.matched_count();
if count > 0 {
self.set_selected_index(0, cx);
self.set_selected_entry_index(0, cx);
}
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
let count = self.matched_count();
if count > 0 {
self.set_selected_index(count - 1, cx);
self.set_selected_entry_index(count - 1, cx);
}
}
fn set_selected_index(&mut self, index: usize, cx: &mut Context<Self>) {
self.selected_index = index;
fn set_selected_entry_index(&mut self, entry_index: usize, cx: &mut Context<Self>) {
self.selected_index = entry_index;
let scroll_ix = match self.search_state {
SearchState::Empty | SearchState::Searching { .. } => self
.separated_item_indexes
.get(entry_index)
.map(|ix| *ix as usize)
.unwrap_or(entry_index + 1),
SearchState::Searched { .. } => entry_index,
};
self.scroll_handle
.scroll_to_item(index, ScrollStrategy::Top);
.scroll_to_item(scroll_ix, ScrollStrategy::Top);
cx.notify();
}
@@ -368,14 +402,12 @@ impl ThreadHistory {
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self.assistant_panel.update(cx, move |this, cx| {
HistoryEntry::Thread(thread) => self.agent_panel.update(cx, move |this, cx| {
this.open_thread_by_id(&thread.id, window, cx)
}),
HistoryEntry::Context(context) => {
self.assistant_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
})
}
HistoryEntry::Context(context) => self.agent_panel.update(cx, move |this, cx| {
this.open_saved_prompt_editor(context.path.clone(), window, cx)
}),
};
if let Some(task) = task_result.log_err() {
@@ -395,10 +427,10 @@ impl ThreadHistory {
if let Some(entry) = self.get_match(self.selected_index) {
let task_result = match entry {
HistoryEntry::Thread(thread) => self
.assistant_panel
.agent_panel
.update(cx, |this, cx| this.delete_thread(&thread.id, cx)),
HistoryEntry::Context(context) => self
.assistant_panel
.agent_panel
.update(cx, |this, cx| this.delete_context(context.path.clone(), cx)),
};
@@ -494,7 +526,7 @@ impl ThreadHistory {
match entry {
HistoryEntry::Thread(thread) => PastThread::new(
thread.clone(),
self.assistant_panel.clone(),
self.agent_panel.clone(),
is_active,
highlight_positions,
format,
@@ -502,7 +534,7 @@ impl ThreadHistory {
.into_any_element(),
HistoryEntry::Context(context) => PastContext::new(
context.clone(),
self.assistant_panel.clone(),
self.agent_panel.clone(),
is_active,
highlight_positions,
format,
@@ -575,7 +607,7 @@ impl Render for ThreadHistory {
uniform_list(
cx.entity().clone(),
"thread-history",
self.matched_count(),
self.list_item_count(),
Self::list_items,
)
.p_1()
@@ -593,7 +625,7 @@ impl Render for ThreadHistory {
#[derive(IntoElement)]
pub struct PastThread {
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
agent_panel: WeakEntity<AgentPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
@@ -602,14 +634,14 @@ pub struct PastThread {
impl PastThread {
pub fn new(
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
agent_panel: WeakEntity<AgentPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
) -> Self {
Self {
thread,
assistant_panel,
agent_panel,
selected,
highlight_positions,
timestamp_format,
@@ -622,7 +654,7 @@ impl RenderOnce for PastThread {
let summary = self.thread.summary;
let thread_timestamp = self.timestamp_format.format_timestamp(
&self.assistant_panel,
&self.agent_panel,
self.thread.updated_at.timestamp(),
cx,
);
@@ -655,10 +687,10 @@ impl RenderOnce for PastThread {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let agent_panel = self.agent_panel.clone();
let id = self.thread.id.clone();
move |_event, _window, cx| {
assistant_panel
agent_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx).detach_and_log_err(cx);
})
@@ -668,10 +700,10 @@ impl RenderOnce for PastThread {
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let agent_panel = self.agent_panel.clone();
let id = self.thread.id.clone();
move |_event, window, cx| {
assistant_panel
agent_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
@@ -685,7 +717,7 @@ impl RenderOnce for PastThread {
#[derive(IntoElement)]
pub struct PastContext {
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
agent_panel: WeakEntity<AgentPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
@@ -694,14 +726,14 @@ pub struct PastContext {
impl PastContext {
pub fn new(
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
agent_panel: WeakEntity<AgentPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
) -> Self {
Self {
context,
assistant_panel,
agent_panel,
selected,
highlight_positions,
timestamp_format,
@@ -713,7 +745,7 @@ impl RenderOnce for PastContext {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.context.title;
let context_timestamp = self.timestamp_format.format_timestamp(
&self.assistant_panel,
&self.agent_panel,
self.context.mtime.timestamp(),
cx,
);
@@ -748,10 +780,10 @@ impl RenderOnce for PastContext {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let agent_panel = self.agent_panel.clone();
let path = self.context.path.clone();
move |_event, _window, cx| {
assistant_panel
agent_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
@@ -762,10 +794,10 @@ impl RenderOnce for PastContext {
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let agent_panel = self.agent_panel.clone();
let path = self.context.path.clone();
move |_event, window, cx| {
assistant_panel
agent_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
@@ -785,12 +817,12 @@ pub enum EntryTimeFormat {
impl EntryTimeFormat {
fn format_timestamp(
&self,
assistant_panel: &WeakEntity<AssistantPanel>,
agent_panel: &WeakEntity<AgentPanel>,
timestamp: i64,
cx: &App,
) -> String {
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
let timezone = assistant_panel
let timezone = agent_panel
.read_with(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC);

View File

@@ -28,6 +28,7 @@ use prompt_store::{
};
use serde::{Deserialize, Serialize};
use settings::{Settings as _, SettingsStore};
use ui::Window;
use util::ResultExt as _;
use crate::context_server_tool::ContextServerTool;
@@ -388,18 +389,20 @@ impl ThreadStore {
pub fn open_thread(
&self,
id: &ThreadId,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Thread>>> {
let id = id.clone();
let database_future = ThreadsDatabase::global_future(cx);
cx.spawn(async move |this, cx| {
let this = cx.weak_entity();
window.spawn(cx, async move |cx| {
let database = database_future.await.map_err(|err| anyhow!(err))?;
let thread = database
.try_find_thread(id.clone())
.await?
.ok_or_else(|| anyhow!("no thread found with ID: {id:?}"))?;
let thread = this.update(cx, |this, cx| {
let thread = this.update_in(cx, |this, window, cx| {
cx.new(|cx| {
Thread::deserialize(
id.clone(),
@@ -408,6 +411,7 @@ impl ThreadStore {
this.tools.clone(),
this.prompt_builder.clone(),
this.project_context.clone(),
window,
cx,
)
})
@@ -653,6 +657,8 @@ pub struct SerializedThread {
pub model: Option<SerializedLanguageModel>,
#[serde(default)]
pub completion_mode: Option<CompletionMode>,
#[serde(default)]
pub profile: Option<AgentProfileId>,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -772,6 +778,7 @@ pub struct SerializedToolResult {
pub tool_use_id: LanguageModelToolUseId,
pub is_error: bool,
pub content: Arc<str>,
pub output: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
@@ -797,6 +804,7 @@ impl LegacySerializedThread {
exceeded_window_error: None,
model: None,
completion_mode: None,
profile: None,
}
}
}

View File

@@ -1,16 +1,17 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{AnyToolCard, Tool, ToolUseStatus, ToolWorkingSet};
use assistant_tool::{AnyToolCard, Tool, ToolResultOutput, ToolUseStatus, ToolWorkingSet};
use collections::HashMap;
use futures::FutureExt as _;
use futures::future::Shared;
use gpui::{App, Entity, SharedString, Task};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelRequestMessage, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
LanguageModelToolUse, LanguageModelToolUseId, Role,
};
use ui::IconName;
use project::Project;
use ui::{IconName, Window};
use util::truncate_lines_to_byte_limit;
use crate::thread::{MessageId, PromptId, ThreadId};
@@ -54,6 +55,9 @@ impl ToolUseState {
pub fn from_serialized_messages(
tools: Entity<ToolWorkingSet>,
messages: &[SerializedMessage],
project: Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Self {
let mut this = Self::new(tools);
let mut tool_names_by_id = HashMap::default();
@@ -93,12 +97,23 @@ impl ToolUseState {
this.tool_results.insert(
tool_use_id.clone(),
LanguageModelToolResult {
tool_use_id,
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.clone(),
is_error: tool_result.is_error,
content: tool_result.content.clone(),
output: tool_result.output.clone(),
},
);
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
if let Some(output) = tool_result.output.clone() {
if let Some(card) =
tool.deserialize_card(output, project.clone(), window, cx)
{
this.tool_result_cards.insert(tool_use_id, card);
}
}
}
}
}
}
@@ -124,6 +139,7 @@ impl ToolUseState {
tool_use_id: tool_use_id.clone(),
tool_name: tool_use.name.clone(),
content,
output: None,
is_error: true,
},
);
@@ -338,7 +354,7 @@ impl ToolUseState {
tool_use_id: LanguageModelToolUseId,
ui_text: impl Into<Arc<str>>,
input: serde_json::Value,
messages: Arc<Vec<LanguageModelRequestMessage>>,
request: Arc<LanguageModelRequest>,
tool: Arc<dyn Tool>,
) {
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
@@ -347,7 +363,7 @@ impl ToolUseState {
let confirmation = Confirmation {
tool_use_id,
input,
messages,
request,
tool,
ui_text,
};
@@ -359,7 +375,7 @@ impl ToolUseState {
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_name: Arc<str>,
output: Result<String>,
output: Result<ToolResultOutput>,
configured_model: Option<&ConfiguredModel>,
) -> Option<PendingToolUse> {
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
@@ -379,7 +395,8 @@ impl ToolUseState {
);
match output {
Ok(tool_result) => {
Ok(output) => {
let tool_result = output.content;
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
// Protect from clearly large output
@@ -406,6 +423,7 @@ impl ToolUseState {
tool_name,
content: tool_result.into(),
is_error: false,
output: output.output,
},
);
self.pending_tool_uses_by_id.remove(&tool_use_id)
@@ -418,6 +436,7 @@ impl ToolUseState {
tool_name,
content: err.to_string().into(),
is_error: true,
output: None,
},
);
@@ -430,71 +449,20 @@ impl ToolUseState {
}
}
pub fn attach_tool_uses(
&self,
message_id: MessageId,
request_message: &mut LanguageModelRequestMessage,
) {
if let Some(tool_uses) = self.tool_uses_by_assistant_message.get(&message_id) {
for tool_use in tool_uses {
if self.tool_results.contains_key(&tool_use.id) {
// Do not send tool uses until they are completed
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
} else {
log::debug!(
"skipped tool use {:?} because it is still pending",
tool_use
);
}
}
}
}
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
self.tool_uses_by_assistant_message
.contains_key(&assistant_message_id)
}
pub fn tool_results_message(
pub fn tool_results(
&self,
assistant_message_id: MessageId,
) -> Option<LanguageModelRequestMessage> {
let tool_uses = self
.tool_uses_by_assistant_message
.get(&assistant_message_id)?;
if tool_uses.is_empty() {
return None;
}
let mut request_message = LanguageModelRequestMessage {
role: Role::User,
content: vec![],
cache: false,
};
for tool_use in tool_uses {
if let Some(tool_result) = self.tool_results.get(&tool_use.id) {
request_message
.content
.push(MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_use.id.clone(),
tool_name: tool_result.tool_name.clone(),
is_error: tool_result.is_error,
content: if tool_result.content.is_empty() {
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
"<Tool returned an empty string>".into()
} else {
tool_result.content.clone()
},
}));
}
}
Some(request_message)
) -> impl Iterator<Item = (&LanguageModelToolUse, Option<&LanguageModelToolResult>)> {
self.tool_uses_by_assistant_message
.get(&assistant_message_id)
.into_iter()
.flatten()
.map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id)))
}
}
@@ -515,7 +483,7 @@ pub struct Confirmation {
pub tool_use_id: LanguageModelToolUseId,
pub input: serde_json::Value,
pub ui_text: Arc<str>,
pub messages: Arc<Vec<LanguageModelRequestMessage>>,
pub request: Arc<LanguageModelRequest>,
pub tool: Arc<dyn Tool>,
}

View File

@@ -4,7 +4,7 @@ use gpui::{
use ui::{TintColor, Vector, VectorName, prelude::*};
use workspace::{ModalView, Workspace};
use crate::assistant_panel::AssistantPanel;
use crate::agent_panel::AgentPanel;
macro_rules! agent_onboarding_event {
($name:expr) => {
@@ -31,7 +31,7 @@ impl AgentOnboardingModal {
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.workspace.update(cx, |workspace, cx| {
workspace.focus_panel::<AssistantPanel>(window, cx);
workspace.focus_panel::<AgentPanel>(window, cx);
});
cx.emit(DismissEvent);
@@ -92,10 +92,10 @@ impl Render for AgentOnboardingModal {
.top_0()
.right(px(-1.0))
.w(px(441.))
.h(px(167.))
.h(px(240.))
.child(
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.))
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(240.))
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.05))),
),
)
.child(
@@ -110,6 +110,23 @@ impl Render for AgentOnboardingModal {
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
),
)
.child(
div()
.absolute()
.inset_0()
.size_full()
.bg(gpui::linear_gradient(
175.,
gpui::linear_color_stop(
cx.theme().colors().elevated_surface_background,
0.,
),
gpui::linear_color_stop(
cx.theme().colors().elevated_surface_background.opacity(0.),
0.8,
),
)),
)
.child(
v_flex()
.w_full()

View File

@@ -578,6 +578,7 @@ pub enum ToolChoice {
Auto,
Any,
Tool { name: String },
None,
}
#[derive(Debug, Serialize, Deserialize)]

View File

@@ -1,88 +0,0 @@
[package]
name = "assistant"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant.rs"
doctest = false
[features]
test-support = [
"editor/test-support",
"language/test-support",
"project/test-support",
"text/test-support",
]
[dependencies]
anyhow.workspace = true
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
log.workspace = true
lsp.workspace = true
menu.workspace = true
multi_buffer.workspace = true
parking_lot.workspace = true
project.workspace = true
rules_library.workspace = true
prompt_store.workspace = true
proto.workspace = true
rope.workspace = true
schemars.workspace = true
search.workspace = true
serde.workspace = true
settings.workspace = true
smol.workspace = true
streaming_diff.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
terminal.workspace = true
terminal_view.workspace = true
text.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
languages = { workspace = true, features = ["test-support"] }
log.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
rand.workspace = true
serde_json_lenient.workspace = true
terminal_view = { workspace = true, features = ["test-support"] }
text = { workspace = true, features = ["test-support"] }
tree-sitter-md.workspace = true
unindent.workspace = true

View File

@@ -1,199 +0,0 @@
use std::sync::Arc;
use collections::HashMap;
use gpui::{AnyView, App, EventEmitter, FocusHandle, Focusable, Subscription, canvas};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use ui::{ElevationIndex, prelude::*};
use workspace::Item;
pub struct ConfigurationView {
focus_handle: FocusHandle,
configuration_views: HashMap<LanguageModelProviderId, AnyView>,
_registry_subscription: Subscription,
}
impl ConfigurationView {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let registry_subscription = cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
|this, _, event: &language_model::Event, window, cx| match event {
language_model::Event::AddedProvider(provider_id) => {
let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
if let Some(provider) = provider {
this.add_configuration_view(&provider, window, cx);
}
}
language_model::Event::RemovedProvider(provider_id) => {
this.remove_configuration_view(provider_id);
}
_ => {}
},
);
let mut this = Self {
focus_handle,
configuration_views: HashMap::default(),
_registry_subscription: registry_subscription,
};
this.build_configuration_views(window, cx);
this
}
fn build_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
for provider in providers {
self.add_configuration_view(&provider, window, cx);
}
}
fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
self.configuration_views.remove(provider_id);
}
fn add_configuration_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let configuration_view = provider.configuration_view(window, cx);
self.configuration_views
.insert(provider.id(), configuration_view);
}
fn render_provider_view(
&mut self,
provider: &Arc<dyn LanguageModelProvider>,
cx: &mut Context<Self>,
) -> Div {
let provider_id = provider.id().0.clone();
let provider_name = provider.name().0.clone();
let configuration_view = self.configuration_views.get(&provider.id()).cloned();
let open_new_context = cx.listener({
let provider = provider.clone();
move |_, _, _window, cx| {
cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
provider.clone(),
))
}
});
v_flex()
.gap_2()
.child(
h_flex()
.justify_between()
.child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
.when(provider.is_authenticated(cx), move |this| {
this.child(
h_flex().justify_end().child(
Button::new(
SharedString::from(format!("new-context-{provider_id}")),
"Open New Chat",
)
.icon_position(IconPosition::Start)
.icon(IconName::Plus)
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(open_new_context),
),
)
}),
)
.child(
div()
.p(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_sm()
.when(configuration_view.is_none(), |this| {
this.child(div().child(Label::new(format!(
"No configuration view for {}",
provider_name
))))
})
.when_some(configuration_view, |this, configuration_view| {
this.child(configuration_view)
}),
)
}
}
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let provider_views = providers
.into_iter()
.map(|provider| self.render_provider_view(&provider, cx))
.collect::<Vec<_>>();
let mut element = v_flex()
.id("assistant-configuration-view")
.track_focus(&self.focus_handle(cx))
.bg(cx.theme().colors().editor_background)
.size_full()
.overflow_y_scroll()
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.gap_1()
.child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
.child(
Label::new(
"At least one LLM provider must be configured to use the Assistant.",
)
.color(Color::Muted),
),
)
.child(
v_flex()
.p(DynamicSpacing::Base16.rems(cx))
.mt_1()
.gap_6()
.flex_1()
.children(provider_views),
)
.into_any();
// We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
// because we couldn't the element to take up the size of the parent.
canvas(
move |bounds, window, cx| {
element.prepaint_as_root(bounds.origin, bounds.size.into(), window, cx);
element
},
|_, mut element, window, cx| {
element.paint(window, cx);
},
)
.flex_1()
.w_full()
}
}
pub enum ConfigurationViewEvent {
NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
}
impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
impl Focusable for ConfigurationView {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for ConfigurationView {
type Event = ConfigurationViewEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Configuration".into()
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -46,7 +46,6 @@ serde_json.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
strum.workspace = true
telemetry_events.workspace = true
text.workspace = true
theme.workspace = true

View File

@@ -2,7 +2,6 @@ mod context;
mod context_editor;
mod context_history;
mod context_store;
mod patch;
mod slash_command;
mod slash_command_picker;
@@ -15,7 +14,6 @@ pub use crate::context::*;
pub use crate::context_editor::*;
pub use crate::context_history::*;
pub use crate::context_store::*;
pub use crate::patch::*;
pub use crate::slash_command::*;
pub fn init(client: Arc<Client>, _cx: &mut App) {

View File

@@ -1,7 +1,6 @@
#[cfg(test)]
mod context_tests;
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings;
use assistant_slash_command::{
@@ -37,7 +36,6 @@ use std::{
iter, mem,
ops::Range,
path::Path,
str::FromStr as _,
sync::Arc,
time::{Duration, Instant},
};
@@ -122,14 +120,6 @@ impl MessageStatus {
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RequestType {
/// Request a normal chat response from the model.
Chat,
/// Add a preamble to the message, which tells the model to return a structured response that suggests edits.
SuggestEdits,
}
#[derive(Clone, Debug)]
pub enum ContextOperation {
InsertMessage {
@@ -464,10 +454,6 @@ pub enum ContextEvent {
StreamedCompletion,
StartedThoughtProcess(Range<language::Anchor>),
EndedThoughtProcess(language::Anchor),
PatchesUpdated {
removed: Vec<Range<language::Anchor>>,
updated: Vec<Range<language::Anchor>>,
},
InvokedSlashCommandChanged {
command_id: InvokedSlashCommandId,
},
@@ -605,26 +591,6 @@ struct PendingCompletion {
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct InvokedSlashCommandId(clock::Lamport);
#[derive(Clone, Debug)]
pub struct XmlTag {
pub kind: XmlTagKind,
pub range: Range<text::Anchor>,
pub is_open_tag: bool,
}
#[derive(Copy, Clone, Debug, strum::EnumString, PartialEq, Eq, strum::AsRefStr)]
#[strum(serialize_all = "snake_case")]
pub enum XmlTagKind {
Patch,
Title,
Edit,
Path,
Description,
OldText,
NewText,
Operation,
}
pub struct AssistantContext {
id: ContextId,
timestamp: clock::Lamport,
@@ -653,8 +619,6 @@ pub struct AssistantContext {
_subscriptions: Vec<Subscription>,
telemetry: Option<Arc<Telemetry>>,
language_registry: Arc<LanguageRegistry>,
patches: Vec<AssistantPatch>,
xml_tags: Vec<XmlTag>,
project: Option<Entity<Project>>,
prompt_builder: Arc<PromptBuilder>,
}
@@ -669,18 +633,6 @@ impl ContextAnnotation for ParsedSlashCommand {
}
}
impl ContextAnnotation for AssistantPatch {
fn range(&self) -> &Range<language::Anchor> {
&self.range
}
}
impl ContextAnnotation for XmlTag {
fn range(&self) -> &Range<language::Anchor> {
&self.range
}
}
impl EventEmitter<ContextEvent> for AssistantContext {}
impl AssistantContext {
@@ -757,8 +709,6 @@ impl AssistantContext {
project,
language_registry,
slash_commands,
patches: Vec::new(),
xml_tags: Vec::new(),
prompt_builder,
};
@@ -1156,48 +1106,6 @@ impl AssistantContext {
self.summary.as_ref()
}
pub fn patch_containing(&self, position: Point, cx: &App) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
let index = self.patches.binary_search_by(|patch| {
let patch_range = patch.range.to_point(&buffer);
if position < patch_range.start {
Ordering::Greater
} else if position > patch_range.end {
Ordering::Less
} else {
Ordering::Equal
}
});
if let Ok(ix) = index {
Some(&self.patches[ix])
} else {
None
}
}
pub fn patch_ranges(&self) -> impl Iterator<Item = Range<language::Anchor>> + '_ {
self.patches.iter().map(|patch| patch.range.clone())
}
pub fn patch_for_range(
&self,
range: &Range<language::Anchor>,
cx: &App,
) -> Option<&AssistantPatch> {
let buffer = self.buffer.read(cx);
let index = self.patch_index_for_range(range, buffer).ok()?;
Some(&self.patches[index])
}
fn patch_index_for_range(
&self,
tagged_range: &Range<text::Anchor>,
buffer: &text::BufferSnapshot,
) -> Result<usize, usize> {
self.patches
.binary_search_by(|probe| probe.range.cmp(&tagged_range, buffer))
}
pub fn parsed_slash_commands(&self) -> &[ParsedSlashCommand] {
&self.parsed_slash_commands
}
@@ -1277,7 +1185,7 @@ impl AssistantContext {
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
return;
};
let request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
let request = self.to_completion_request(Some(&model.model), cx);
let debounce = self.token_count.is_some();
self.pending_token_count = cx.spawn(async move |this, cx| {
async move {
@@ -1423,7 +1331,7 @@ impl AssistantContext {
}
let request = {
let mut req = self.to_completion_request(Some(&model), RequestType::Chat, cx);
let mut req = self.to_completion_request(Some(&model), cx);
// Skip the last message because it's likely to change and
// therefore would be a waste to cache.
req.messages.pop();
@@ -1498,8 +1406,6 @@ impl AssistantContext {
let mut removed_parsed_slash_command_ranges = Vec::new();
let mut updated_parsed_slash_commands = Vec::new();
let mut removed_patches = Vec::new();
let mut updated_patches = Vec::new();
while let Some(mut row_range) = row_ranges.next() {
while let Some(next_row_range) = row_ranges.peek() {
if row_range.end >= next_row_range.start {
@@ -1524,13 +1430,6 @@ impl AssistantContext {
cx,
);
self.invalidate_pending_slash_commands(&buffer, cx);
self.reparse_patches_in_range(
start..end,
&buffer,
&mut updated_patches,
&mut removed_patches,
cx,
);
}
if !updated_parsed_slash_commands.is_empty()
@@ -1541,13 +1440,6 @@ impl AssistantContext {
updated: updated_parsed_slash_commands,
});
}
if !updated_patches.is_empty() || !removed_patches.is_empty() {
cx.emit(ContextEvent::PatchesUpdated {
removed: removed_patches,
updated: updated_patches,
});
}
}
fn reparse_slash_commands_in_range(
@@ -1638,267 +1530,6 @@ impl AssistantContext {
}
}
fn reparse_patches_in_range(
&mut self,
range: Range<text::Anchor>,
buffer: &BufferSnapshot,
updated: &mut Vec<Range<text::Anchor>>,
removed: &mut Vec<Range<text::Anchor>>,
cx: &mut Context<Self>,
) {
// Rebuild the XML tags in the edited range.
let intersecting_tags_range =
self.indices_intersecting_buffer_range(&self.xml_tags, range.clone(), cx);
let new_tags = self.parse_xml_tags_in_range(buffer, range.clone(), cx);
self.xml_tags
.splice(intersecting_tags_range.clone(), new_tags);
// Find which patches intersect the changed range.
let intersecting_patches_range =
self.indices_intersecting_buffer_range(&self.patches, range.clone(), cx);
// Reparse all tags after the last unchanged patch before the change.
let mut tags_start_ix = 0;
if let Some(preceding_unchanged_patch) =
self.patches[..intersecting_patches_range.start].last()
{
tags_start_ix = match self.xml_tags.binary_search_by(|tag| {
tag.range
.start
.cmp(&preceding_unchanged_patch.range.end, buffer)
.then(Ordering::Less)
}) {
Ok(ix) | Err(ix) => ix,
};
}
// Rebuild the patches in the range.
let new_patches = self.parse_patches(tags_start_ix, range.end, buffer, cx);
updated.extend(new_patches.iter().map(|patch| patch.range.clone()));
let removed_patches = self.patches.splice(intersecting_patches_range, new_patches);
removed.extend(
removed_patches
.map(|patch| patch.range)
.filter(|range| !updated.contains(&range)),
);
}
fn parse_xml_tags_in_range(
&self,
buffer: &BufferSnapshot,
range: Range<text::Anchor>,
cx: &App,
) -> Vec<XmlTag> {
let mut messages = self.messages(cx).peekable();
let mut tags = Vec::new();
let mut lines = buffer.text_for_range(range).lines();
let mut offset = lines.offset();
while let Some(line) = lines.next() {
while let Some(message) = messages.peek() {
if offset < message.offset_range.end {
break;
} else {
messages.next();
}
}
let is_assistant_message = messages
.peek()
.map_or(false, |message| message.role == Role::Assistant);
if is_assistant_message {
for (start_ix, _) in line.match_indices('<') {
let mut name_start_ix = start_ix + 1;
let closing_bracket_ix = line[start_ix..].find('>').map(|i| start_ix + i);
if let Some(closing_bracket_ix) = closing_bracket_ix {
let end_ix = closing_bracket_ix + 1;
let mut is_open_tag = true;
if line[name_start_ix..closing_bracket_ix].starts_with('/') {
name_start_ix += 1;
is_open_tag = false;
}
let tag_inner = &line[name_start_ix..closing_bracket_ix];
let tag_name_len = tag_inner
.find(|c: char| c.is_whitespace())
.unwrap_or(tag_inner.len());
if let Ok(kind) = XmlTagKind::from_str(&tag_inner[..tag_name_len]) {
tags.push(XmlTag {
range: buffer.anchor_after(offset + start_ix)
..buffer.anchor_before(offset + end_ix),
is_open_tag,
kind,
});
};
}
}
}
offset = lines.offset();
}
tags
}
fn parse_patches(
&mut self,
tags_start_ix: usize,
buffer_end: text::Anchor,
buffer: &BufferSnapshot,
cx: &App,
) -> Vec<AssistantPatch> {
let mut new_patches = Vec::new();
let mut pending_patch = None;
let mut patch_tag_depth = 0;
let mut tags = self.xml_tags[tags_start_ix..].iter().peekable();
'tags: while let Some(tag) = tags.next() {
if tag.range.start.cmp(&buffer_end, buffer).is_gt() && patch_tag_depth == 0 {
break;
}
if tag.kind == XmlTagKind::Patch && tag.is_open_tag {
patch_tag_depth += 1;
let patch_start = tag.range.start;
let mut edits = Vec::<Result<AssistantEdit>>::new();
let mut patch = AssistantPatch {
range: patch_start..patch_start,
title: String::new().into(),
edits: Default::default(),
status: crate::AssistantPatchStatus::Pending,
};
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Patch && !tag.is_open_tag {
patch_tag_depth -= 1;
if patch_tag_depth == 0 {
patch.range.end = tag.range.end;
// Include the line immediately after this <patch> tag if it's empty.
let patch_end_offset = patch.range.end.to_offset(buffer);
let mut patch_end_chars = buffer.chars_at(patch_end_offset);
if patch_end_chars.next() == Some('\n')
&& patch_end_chars.next().map_or(true, |ch| ch == '\n')
{
let messages = self.messages_for_offsets(
[patch_end_offset, patch_end_offset + 1],
cx,
);
if messages.len() == 1 {
patch.range.end = buffer.anchor_before(patch_end_offset + 1);
}
}
edits.sort_unstable_by(|a, b| {
if let (Ok(a), Ok(b)) = (a, b) {
a.path.cmp(&b.path)
} else {
Ordering::Equal
}
});
patch.edits = edits.into();
patch.status = AssistantPatchStatus::Ready;
new_patches.push(patch);
continue 'tags;
}
}
if tag.kind == XmlTagKind::Title && tag.is_open_tag {
let content_start = tag.range.end;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Title && !tag.is_open_tag {
let content_end = tag.range.start;
patch.title =
trimmed_text_in_range(buffer, content_start..content_end)
.into();
break;
}
}
}
if tag.kind == XmlTagKind::Edit && tag.is_open_tag {
let mut path = None;
let mut old_text = None;
let mut new_text = None;
let mut operation = None;
let mut description = None;
while let Some(tag) = tags.next() {
if tag.kind == XmlTagKind::Edit && !tag.is_open_tag {
edits.push(AssistantEdit::new(
path,
operation,
old_text,
new_text,
description,
));
break;
}
if tag.is_open_tag
&& [
XmlTagKind::Path,
XmlTagKind::OldText,
XmlTagKind::NewText,
XmlTagKind::Operation,
XmlTagKind::Description,
]
.contains(&tag.kind)
{
let kind = tag.kind;
let content_start = tag.range.end;
if let Some(tag) = tags.peek() {
if tag.kind == kind && !tag.is_open_tag {
let tag = tags.next().unwrap();
let content_end = tag.range.start;
let content = trimmed_text_in_range(
buffer,
content_start..content_end,
);
match kind {
XmlTagKind::Path => path = Some(content),
XmlTagKind::Operation => operation = Some(content),
XmlTagKind::OldText => {
old_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::NewText => {
new_text = Some(content).filter(|s| !s.is_empty())
}
XmlTagKind::Description => {
description =
Some(content).filter(|s| !s.is_empty())
}
_ => {}
}
}
}
}
}
}
}
patch.edits = edits.into();
pending_patch = Some(patch);
}
}
if let Some(mut pending_patch) = pending_patch {
let patch_start = pending_patch.range.start.to_offset(buffer);
if let Some(message) = self.message_for_offset(patch_start, cx) {
if message.anchor_range.end == text::Anchor::MAX {
pending_patch.range.end = text::Anchor::MAX;
} else {
let message_end = buffer.anchor_after(message.offset_range.end - 1);
pending_patch.range.end = message_end;
}
} else {
pending_patch.range.end = text::Anchor::MAX;
}
new_patches.push(pending_patch);
}
new_patches
}
pub fn pending_command_for_position(
&mut self,
position: language::Anchor,
@@ -2303,11 +1934,7 @@ impl AssistantContext {
})
}
pub fn assist(
&mut self,
request_type: RequestType,
cx: &mut Context<Self>,
) -> Option<MessageAnchor> {
pub fn assist(&mut self, cx: &mut Context<Self>) -> Option<MessageAnchor> {
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.default_model()?;
let last_message_id = self.get_last_valid_message_id(cx)?;
@@ -2322,7 +1949,7 @@ impl AssistantContext {
// Compute which messages to cache, including the last one.
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
let request = self.to_completion_request(Some(&model), request_type, cx);
let request = self.to_completion_request(Some(&model), cx);
let assistant_message = self
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
@@ -2563,7 +2190,6 @@ impl AssistantContext {
pub fn to_completion_request(
&self,
model: Option<&Arc<dyn LanguageModel>>,
request_type: RequestType,
cx: &App,
) -> LanguageModelRequest {
let buffer = self.buffer.read(cx);
@@ -2585,6 +2211,7 @@ impl AssistantContext {
mode: None,
messages: Vec::new(),
tools: Vec::new(),
tool_choice: None,
stop: Vec::new(),
temperature: model
.and_then(|model| AssistantSettings::temperature_for_model(model, cx)),
@@ -2643,25 +2270,6 @@ impl AssistantContext {
}
}
if let RequestType::SuggestEdits = request_type {
if let Ok(preamble) = self.prompt_builder.generate_suggest_edits_prompt() {
let last_elem_index = completion_request.messages.len();
completion_request
.messages
.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text(preamble)],
cache: false,
});
// The preamble message should be sent right before the last actual user message.
completion_request
.messages
.swap(last_elem_index, last_elem_index.saturating_sub(1));
}
}
completion_request
}
@@ -2696,17 +2304,6 @@ impl AssistantContext {
ranges.push(message.anchor_range.clone());
}
}
let buffer = self.buffer.read(cx).text_snapshot();
let mut updated = Vec::new();
let mut removed = Vec::new();
for range in ranges {
self.reparse_patches_in_range(range, &buffer, &mut updated, &mut removed, cx);
}
if !updated.is_empty() || !removed.is_empty() {
cx.emit(ContextEvent::PatchesUpdated { removed, updated })
}
}
pub fn update_metadata(
@@ -2984,7 +2581,7 @@ impl AssistantContext {
return;
}
let mut request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
let mut request = self.to_completion_request(Some(&model.model), cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
@@ -3243,24 +2840,6 @@ impl AssistantContext {
}
}
fn trimmed_text_in_range(buffer: &BufferSnapshot, range: Range<text::Anchor>) -> String {
let mut is_start = true;
let mut content = buffer
.text_for_range(range)
.map(|mut chunk| {
if is_start {
chunk = chunk.trim_start_matches('\n');
if !chunk.is_empty() {
is_start = false;
}
}
chunk
})
.collect::<String>();
content.truncate(content.trim_end().len());
content
}
#[derive(Debug, Default)]
pub struct ContextVersion {
context: clock::Global,

View File

@@ -1,6 +1,6 @@
use crate::{
AssistantContext, AssistantEdit, AssistantEditKind, CacheStatus, ContextEvent, ContextId,
ContextOperation, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation,
InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus,
};
use anyhow::Result;
use assistant_slash_command::{
@@ -32,13 +32,10 @@ use std::{
rc::Rc,
sync::{Arc, atomic::AtomicBool},
};
use text::{OffsetRangeExt as _, ReplicaId, ToOffset, network::Network};
use text::{ReplicaId, ToOffset, network::Network};
use ui::{IconName, Window};
use unindent::Unindent;
use util::{
RandomCharIter,
test::{generate_marked_text, marked_text_ranges},
};
use util::RandomCharIter;
use workspace::Workspace;
#[gpui::test]
@@ -664,401 +661,6 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
}
}
#[gpui::test]
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
cx.update(|cx| {
init_test(cx);
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store
.set_user_settings(
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
cx,
)
.unwrap()
})
});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [Path::new("/root")], cx).await;
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
// Create a new context
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context = cx.new(|cx| {
AssistantContext::local(
registry.clone(),
Some(project),
None,
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
cx,
)
});
// Insert an assistant message to simulate a response.
let assistant_message_id = context.update(cx, |context, cx| {
let user_message_id = context.messages(cx).next().unwrap().id;
context
.insert_message_after(user_message_id, Role::Assistant, MessageStatus::Done, cx)
.unwrap()
.id
});
// No edit tags
edit(
&context,
"
«one
two
»",
cx,
);
expect_patches(
&context,
"
one
two
",
&[],
cx,
);
// Partial edit step tag is added
edit(
&context,
"
one
two
«
<patch»",
cx,
);
expect_patches(
&context,
"
one
two
<patch",
&[],
cx,
);
// The rest of the step tag is added. The unclosed
// step is treated as incomplete.
edit(
&context,
"
one
two
<patch«>
<edit>»",
cx,
);
expect_patches(
&context,
"
one
two
«<patch>
<edit>»",
&[&[]],
cx,
);
// The full patch is added
edit(
&context,
"
one
two
<patch>
<edit>«
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn one</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
also,»",
cx,
);
expect_patches(
&context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn one</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn one".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
// The step is manually edited.
edit(
&context,
"
one
two
<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>«fn zero»</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
also,",
cx,
);
expect_patches(
&context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
// When setting the message role to User, the steps are cleared.
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
&context,
"
one
two
<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
also,",
&[],
cx,
);
// When setting the message role back to Assistant, the steps are reparsed.
context.update(cx, |context, cx| {
context.cycle_message_roles(HashSet::from_iter([assistant_message_id]), cx);
});
expect_patches(
&context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
// Ensure steps are re-parsed when deserializing.
let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx));
let deserialized_context = cx.new(|cx| {
AssistantContext::deserialize(
serialized_context,
Path::new("").into(),
registry.clone(),
prompt_builder.clone(),
Arc::new(SlashCommandWorkingSet::default()),
None,
None,
cx,
)
});
expect_patches(
&deserialized_context,
"
one
two
«<patch>
<edit>
<description>add a `two` function</description>
<path>src/lib.rs</path>
<operation>insert_after</operation>
<old_text>fn zero</old_text>
<new_text>
fn two() {}
</new_text>
</edit>
</patch>
»
also,",
&[&[AssistantEdit {
path: "src/lib.rs".into(),
kind: AssistantEditKind::InsertAfter {
old_text: "fn zero".into(),
new_text: "fn two() {}".into(),
description: Some("add a `two` function".into()),
},
}]],
cx,
);
fn edit(
context: &Entity<AssistantContext>,
new_text_marked_with_edits: &str,
cx: &mut TestAppContext,
) {
context.update(cx, |context, cx| {
context.buffer.update(cx, |buffer, cx| {
buffer.edit_via_marked_text(&new_text_marked_with_edits.unindent(), None, cx);
});
});
cx.executor().run_until_parked();
}
#[track_caller]
fn expect_patches(
context: &Entity<AssistantContext>,
expected_marked_text: &str,
expected_suggestions: &[&[AssistantEdit]],
cx: &mut TestAppContext,
) {
let expected_marked_text = expected_marked_text.unindent();
let (expected_text, _) = marked_text_ranges(&expected_marked_text, false);
let (buffer_text, ranges, patches) = context.update(cx, |context, cx| {
context.buffer.read_with(cx, |buffer, _| {
let ranges = context
.patches
.iter()
.map(|entry| entry.range.to_offset(buffer))
.collect::<Vec<_>>();
(
buffer.text(),
ranges,
context
.patches
.iter()
.map(|step| step.edits.clone())
.collect::<Vec<_>>(),
)
})
});
assert_eq!(buffer_text, expected_text);
let actual_marked_text = generate_marked_text(&expected_text, &ranges, false);
assert_eq!(actual_marked_text, expected_marked_text);
assert_eq!(
patches
.iter()
.map(|patch| {
patch
.iter()
.map(|edit| {
let edit = edit.as_ref().unwrap();
AssistantEdit {
path: edit.path.clone(),
kind: edit.kind.clone(),
}
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>(),
expected_suggestions
);
}
}
#[gpui::test]
async fn test_serialization(cx: &mut TestAppContext) {
cx.update(init_test);

View File

@@ -9,11 +9,11 @@ use client::{proto, zed_urls};
use collections::{BTreeSet, HashMap, HashSet, hash_map};
use editor::{
Anchor, Editor, EditorEvent, MenuInlineCompletionsPolicy, MultiBuffer, MultiBufferSnapshot,
ProposedChangeLocation, ProposedChangesEditor, RowExt, ToOffset as _, ToPoint,
RowExt, ToOffset as _, ToPoint,
actions::{MoveToEndOfLine, Newline, ShowCompletions},
display_map::{
BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
},
scroll::Autoscroll,
};
@@ -21,12 +21,11 @@ use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
use futures::FutureExt;
use gpui::{
Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardEntry,
ClipboardItem, CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight,
Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage,
SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
WeakEntity, actions, div, img, impl_internal_actions, percentage, point, prelude::*,
pulsating_between, size,
Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem, Empty,
Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement,
IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size,
StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions,
div, img, impl_internal_actions, percentage, point, prelude::*, pulsating_between, size,
};
use indexed_docs::IndexedDocsStore;
use language::{
@@ -69,14 +68,14 @@ use workspace::{
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane::{self, SaveIntent},
pane,
searchable::{SearchEvent, SearchableItem},
};
use crate::{
AssistantContext, AssistantPatch, AssistantPatchStatus, CacheStatus, Content, ContextEvent,
ContextId, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
MessageMetadata, MessageStatus, ParsedSlashCommand, PendingSlashCommandStatus, RequestType,
AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId,
InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus,
ParsedSlashCommand, PendingSlashCommandStatus,
};
use crate::{
ThoughtProcessOutputSection, slash_command::SlashCommandCompletionProvider,
@@ -90,7 +89,6 @@ actions!(
ConfirmCommand,
CopyCode,
CycleMessageRole,
Edit,
InsertIntoEditor,
QuoteSelection,
Split,
@@ -111,22 +109,10 @@ struct ScrollPosition {
cursor: Anchor,
}
struct PatchViewState {
crease_id: CreaseId,
editor: Option<PatchEditorState>,
update_task: Option<Task<()>>,
}
struct PatchEditorState {
editor: WeakEntity<ProposedChangesEditor>,
opened_patch: AssistantPatch,
}
type MessageHeader = MessageMetadata;
#[derive(Clone)]
enum AssistError {
FileRequired,
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
@@ -137,7 +123,7 @@ pub enum ThoughtProcessStatus {
Completed,
}
pub trait AssistantPanelDelegate {
pub trait AgentPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
@@ -171,7 +157,7 @@ pub trait AssistantPanelDelegate {
);
}
impl dyn AssistantPanelDelegate {
impl dyn AgentPanelDelegate {
/// Returns the global [`AssistantPanelDelegate`], if it exists.
pub fn try_global(cx: &App) -> Option<Arc<Self>> {
cx.try_global::<GlobalAssistantPanelDelegate>()
@@ -184,7 +170,7 @@ impl dyn AssistantPanelDelegate {
}
}
struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
impl Global for GlobalAssistantPanelDelegate {}
@@ -204,8 +190,6 @@ pub struct ContextEditor {
pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
_subscriptions: Vec<Subscription>,
patches: HashMap<Range<language::Anchor>, PatchViewState>,
active_patch: Option<Range<language::Anchor>>,
last_error: Option<AssistError>,
show_accept_terms: bool,
pub(crate) slash_menu_handle:
@@ -242,9 +226,9 @@ impl ContextEditor {
let editor = cx.new(|cx| {
let mut editor =
Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_show_line_numbers(false, cx);
editor.set_show_scrollbars(false, cx);
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_runnables(false, cx);
@@ -274,7 +258,6 @@ impl ContextEditor {
let slash_command_sections = context.read(cx).slash_command_output_sections().to_vec();
let thought_process_sections = context.read(cx).thought_process_output_sections().to_vec();
let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
let slash_commands = context.read(cx).slash_commands().clone();
let mut this = Self {
context,
@@ -292,8 +275,6 @@ impl ContextEditor {
pending_slash_command_creases: HashMap::default(),
invoked_slash_command_creases: HashMap::default(),
_subscriptions,
patches: HashMap::default(),
active_patch: None,
last_error: None,
show_accept_terms: false,
slash_menu_handle: Default::default(),
@@ -324,7 +305,6 @@ impl ContextEditor {
window,
cx,
);
this.patches_updated(&Vec::new(), &patch_ranges, window, cx);
this
}
@@ -367,34 +347,13 @@ impl ContextEditor {
}
fn assist(&mut self, _: &Assist, window: &mut Window, cx: &mut Context<Self>) {
self.send_to_model(RequestType::Chat, window, cx);
}
fn edit(&mut self, _: &Edit, window: &mut Window, cx: &mut Context<Self>) {
self.send_to_model(RequestType::SuggestEdits, window, cx);
}
fn focus_active_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
if let Some((_range, patch)) = self.active_patch() {
if let Some(editor) = patch
.editor
.as_ref()
.and_then(|state| state.editor.upgrade())
{
editor.focus_handle(cx).focus(window);
return true;
}
if self.sending_disabled(cx) {
return;
}
false
self.send_to_model(window, cx);
}
fn send_to_model(
&mut self,
request_type: RequestType,
window: &mut Window,
cx: &mut Context<Self>,
) {
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
@@ -407,19 +366,9 @@ impl ContextEditor {
return;
}
if self.focus_active_patch(window, cx) {
return;
}
self.last_error = None;
if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
self.last_error = Some(AssistError::FileRequired);
cx.notify();
} else if let Some(user_message) = self
.context
.update(cx, |context, cx| context.assist(request_type, cx))
{
if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
let new_selection = {
let cursor = user_message
.start
@@ -684,9 +633,6 @@ impl ContextEditor {
}
});
}
ContextEvent::PatchesUpdated { removed, updated } => {
self.patches_updated(removed, updated, window, cx);
}
ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -891,131 +837,6 @@ impl ContextEditor {
});
}
fn patches_updated(
&mut self,
removed: &Vec<Range<text::Anchor>>,
updated: &Vec<Range<text::Anchor>>,
window: &mut Window,
cx: &mut Context<ContextEditor>,
) {
let this = cx.entity().downgrade();
let mut editors_to_close = Vec::new();
self.editor.update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
let multibuffer = &snapshot.buffer_snapshot;
let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
let mut removed_crease_ids = Vec::new();
let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
for range in removed {
if let Some(state) = self.patches.remove(range) {
let patch_start = multibuffer
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
let patch_end = multibuffer
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap();
editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
ranges_to_unfold.push(patch_start..patch_end);
removed_crease_ids.push(state.crease_id);
}
}
editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
editor.remove_creases(removed_crease_ids, cx);
for range in updated {
let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
continue;
};
let path_count = patch.path_count();
let patch_start = multibuffer
.anchor_in_excerpt(excerpt_id, patch.range.start)
.unwrap();
let patch_end = multibuffer
.anchor_in_excerpt(excerpt_id, patch.range.end)
.unwrap();
let render_block: RenderBlock = Arc::new({
let this = this.clone();
let patch_range = range.clone();
move |cx: &mut BlockContext| {
let max_width = cx.max_width;
let gutter_width = cx.gutter_dimensions.full_width();
let block_id = cx.block_id;
let selected = cx.selected;
let window = &mut cx.window;
this.update(cx.app, |this, cx| {
this.render_patch_block(
patch_range.clone(),
max_width,
gutter_width,
block_id,
selected,
window,
cx,
)
})
.ok()
.flatten()
.unwrap_or_else(|| Empty.into_any())
}
});
let height = path_count as u32 + 1;
let crease = Crease::block(
patch_start..patch_end,
height,
BlockStyle::Flex,
render_block.clone(),
);
let should_refold;
if let Some(state) = self.patches.get_mut(&range) {
if let Some(editor_state) = &state.editor {
if editor_state.opened_patch != patch {
state.update_task = Some({
let this = this.clone();
cx.spawn_in(window, async move |_, cx| {
Self::update_patch_editor(this.clone(), patch, cx)
.await
.log_err();
})
});
}
}
should_refold =
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
} else {
let crease_id = editor.insert_creases([crease.clone()], cx)[0];
self.patches.insert(
range.clone(),
PatchViewState {
crease_id,
editor: None,
update_task: None,
},
);
should_refold = true;
}
if should_refold {
editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
editor.fold_creases(vec![crease], false, window, cx);
}
}
});
for editor in editors_to_close {
self.close_patch_editor(editor, window, cx);
}
self.update_active_patch(window, cx);
}
fn insert_thought_process_output_sections(
&mut self,
sections: impl IntoIterator<
@@ -1144,177 +965,12 @@ impl ContextEditor {
}
EditorEvent::SelectionsChanged { .. } => {
self.scroll_position = self.cursor_scroll_position(window, cx);
self.update_active_patch(window, cx);
}
_ => {}
}
cx.emit(event.clone());
}
fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
let patch = self.active_patch.as_ref()?;
Some((patch.clone(), self.patches.get(&patch)?))
}
fn update_active_patch(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let newest_cursor = self.editor.update(cx, |editor, cx| {
editor.selections.newest::<Point>(cx).head()
});
let context = self.context.read(cx);
let new_patch = context.patch_containing(newest_cursor, cx).cloned();
if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
return;
}
if let Some(old_patch_range) = self.active_patch.take() {
if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
if let Some(state) = patch_state.editor.take() {
if let Some(editor) = state.editor.upgrade() {
self.close_patch_editor(editor, window, cx);
}
}
}
}
if let Some(new_patch) = new_patch {
self.active_patch = Some(new_patch.range.clone());
if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
let mut editor = None;
if let Some(state) = &patch_state.editor {
if let Some(opened_editor) = state.editor.upgrade() {
editor = Some(opened_editor);
}
}
if let Some(editor) = editor {
self.workspace
.update(cx, |workspace, cx| {
workspace.activate_item(&editor, true, false, window, cx);
})
.ok();
} else {
patch_state.update_task = Some(cx.spawn_in(window, async move |this, cx| {
Self::open_patch_editor(this, new_patch, cx).await.log_err();
}));
}
}
}
}
fn close_patch_editor(
&mut self,
editor: Entity<ProposedChangesEditor>,
window: &mut Window,
cx: &mut Context<ContextEditor>,
) {
self.workspace
.update(cx, |workspace, cx| {
if let Some(pane) = workspace.pane_for(&editor) {
pane.update(cx, |pane, cx| {
let item_id = editor.entity_id();
if !editor.read(cx).focus_handle(cx).is_focused(window) {
pane.close_item_by_id(item_id, SaveIntent::Skip, window, cx)
.detach_and_log_err(cx);
}
});
}
})
.ok();
}
async fn open_patch_editor(
this: WeakEntity<Self>,
patch: AssistantPatch,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.read_with(cx, |this, _| this.project.clone())?;
let resolved_patch = patch.resolve(project.clone(), cx).await;
let editor = cx.new_window_entity(|window, cx| {
let editor = ProposedChangesEditor::new(
patch.title.clone(),
resolved_patch
.edit_groups
.iter()
.map(|(buffer, groups)| ProposedChangeLocation {
buffer: buffer.clone(),
ranges: groups
.iter()
.map(|group| group.context_range.clone())
.collect(),
})
.collect(),
Some(project.clone()),
window,
cx,
);
resolved_patch.apply(&editor, cx);
editor
})?;
this.update(cx, |this, _| {
if let Some(patch_state) = this.patches.get_mut(&patch.range) {
patch_state.editor = Some(PatchEditorState {
editor: editor.downgrade(),
opened_patch: patch,
});
patch_state.update_task.take();
}
})?;
this.read_with(cx, |this, _| this.workspace.clone())?
.update_in(cx, |workspace, window, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, window, cx)
})
.log_err();
Ok(())
}
async fn update_patch_editor(
this: WeakEntity<Self>,
patch: AssistantPatch,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let project = this.update(cx, |this, _| this.project.clone())?;
let resolved_patch = patch.resolve(project.clone(), cx).await;
this.update_in(cx, |this, window, cx| {
let patch_state = this.patches.get_mut(&patch.range)?;
let locations = resolved_patch
.edit_groups
.iter()
.map(|(buffer, groups)| ProposedChangeLocation {
buffer: buffer.clone(),
ranges: groups
.iter()
.map(|group| group.context_range.clone())
.collect(),
})
.collect();
if let Some(state) = &mut patch_state.editor {
if let Some(editor) = state.editor.upgrade() {
editor.update(cx, |editor, cx| {
editor.set_title(patch.title.clone(), cx);
editor.reset_locations(locations, window, cx);
resolved_patch.apply(editor, cx);
});
state.opened_patch = patch;
} else {
patch_state.editor.take();
}
}
patch_state.update_task.take();
Some(())
})?;
Ok(())
}
fn handle_editor_search_event(
&mut self,
_: &Entity<Editor>,
@@ -1488,7 +1144,7 @@ impl ContextEditor {
h_flex()
.id(("message_header", message_id.as_u64()))
.pl(cx.gutter_dimensions.full_width())
.pl(cx.margins.gutter.full_width())
.h_11()
.w_full()
.relative()
@@ -1583,6 +1239,7 @@ impl ContextEditor {
),
priority: usize::MAX,
render: render_block(MessageMetadata::from(message)),
render_in_minimap: false,
};
let mut new_blocks = vec![];
let mut block_index_to_message = vec![];
@@ -1665,11 +1322,11 @@ impl ContextEditor {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
return;
};
let Some(context_editor_view) =
assistant_panel_delegate.active_context_editor(workspace, window, cx)
agent_panel_delegate.active_context_editor(workspace, window, cx)
else {
return;
};
@@ -1695,9 +1352,9 @@ impl ContextEditor {
cx: &mut Context<Workspace>,
) {
let result = maybe!({
let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
let context_editor_view =
assistant_panel_delegate.active_context_editor(workspace, window, cx)?;
agent_panel_delegate.active_context_editor(workspace, window, cx)?;
Self::get_selection_or_code_block(&context_editor_view, cx)
});
let Some((text, is_code_block)) = result else {
@@ -1730,11 +1387,11 @@ impl ContextEditor {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
return;
};
let Some(context_editor_view) =
assistant_panel_delegate.active_context_editor(workspace, window, cx)
agent_panel_delegate.active_context_editor(workspace, window, cx)
else {
return;
};
@@ -1820,7 +1477,7 @@ impl ContextEditor {
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
return;
};
@@ -1851,7 +1508,7 @@ impl ContextEditor {
return;
}
assistant_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
agent_panel_delegate.quote_selection(workspace, selections, buffer, window, cx);
}
pub fn quote_ranges(
@@ -2157,12 +1814,12 @@ impl ContextEditor {
let image_size = size_for_image(
&image,
size(
cx.max_width - cx.gutter_dimensions.full_width(),
cx.max_width - cx.margins.gutter.full_width(),
MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
),
);
h_flex()
.pl(cx.gutter_dimensions.full_width())
.pl(cx.margins.gutter.full_width())
.child(
img(image.clone())
.object_fit(gpui::ObjectFit::ScaleDown)
@@ -2172,6 +1829,7 @@ impl ContextEditor {
.into_any_element()
}),
priority: 0,
render_in_minimap: false,
})
})
.collect::<Vec<_>>();
@@ -2205,118 +1863,6 @@ impl ContextEditor {
self.context.read(cx).summary_or_default()
}
fn render_patch_block(
&mut self,
range: Range<text::Anchor>,
max_width: Pixels,
gutter_width: Pixels,
id: BlockId,
selected: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<AnyElement> {
let snapshot = self
.editor
.update(cx, |editor, cx| editor.snapshot(window, cx));
let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
let excerpt_id = *excerpt_id;
let anchor = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap();
let theme = cx.theme().clone();
let patch = self.context.read(cx).patch_for_range(&range, cx)?;
let paths = patch
.paths()
.map(|p| SharedString::from(p.to_string()))
.collect::<BTreeSet<_>>();
Some(
v_flex()
.id(id)
.bg(theme.colors().editor_background)
.ml(gutter_width)
.pb_1()
.w(max_width - gutter_width)
.rounded_sm()
.border_1()
.border_color(theme.colors().border_variant)
.overflow_hidden()
.hover(|style| style.border_color(theme.colors().text_accent))
.when(selected, |this| {
this.border_color(theme.colors().text_accent)
})
.cursor(CursorStyle::PointingHand)
.on_click(cx.listener(move |this, _, window, cx| {
this.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |selections| {
selections.select_ranges(vec![anchor..anchor]);
});
});
this.focus_active_patch(window, cx);
}))
.child(
div()
.px_2()
.py_1()
.overflow_hidden()
.text_ellipsis()
.border_b_1()
.border_color(theme.colors().border_variant)
.bg(theme.colors().element_background)
.child(
Label::new(patch.title.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.children(paths.into_iter().map(|path| {
h_flex()
.px_2()
.pt_1()
.gap_1p5()
.child(Icon::new(IconName::File).size(IconSize::Small))
.child(Label::new(path).size(LabelSize::Small))
}))
.when(patch.status == AssistantPatchStatus::Pending, |div| {
div.child(
h_flex()
.pt_1()
.px_2()
.gap_1()
.child(
Icon::new(IconName::ArrowCircle)
.size(IconSize::XSmall)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| {
icon.transform(Transformation::rotate(percentage(
delta,
)))
},
),
)
.child(
Label::new("Generating…")
.color(Color::Muted)
.size(LabelSize::Small)
.with_animation(
"pulsating-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
),
),
)
})
.into_any(),
)
}
fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
// This was previously gated behind the `zed-pro` feature flag. Since we
// aren't planning to ship that right now, we're just hard-coding this
@@ -2436,29 +1982,14 @@ impl ContextEditor {
None => (ButtonStyle::Filled, None),
};
let model = LanguageModelRegistry::read_global(cx).default_model();
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& model
.as_ref()
.map_or(false, |model| model.provider.must_accept_terms(cx));
let disabled = has_configuration_error || needs_to_accept_terms;
ButtonLike::new("send_button")
.disabled(disabled)
.disabled(self.sending_disabled(cx))
.style(style)
.when_some(tooltip, |button, tooltip| {
button.tooltip(move |_, _| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new(
if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
"Chat"
} else {
"Send"
},
))
.child(Label::new("Send"))
.children(
KeyBinding::for_action_in(&Assist, &focus_handle, window, cx)
.map(|binding| binding.into_any_element()),
@@ -2468,59 +1999,18 @@ impl ContextEditor {
})
}
fn render_edit_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx).clone();
let (style, tooltip) = match token_state(&self.context, cx) {
Some(TokenState::NoTokensLeft { .. }) => (
ButtonStyle::Tinted(TintColor::Error),
Some(Tooltip::text("Token limit reached")(window, cx)),
),
Some(TokenState::HasMoreTokens {
over_warn_threshold,
..
}) => {
let (style, tooltip) = if over_warn_threshold {
(
ButtonStyle::Tinted(TintColor::Warning),
Some(Tooltip::text("Token limit is close to exhaustion")(
window, cx,
)),
)
} else {
(ButtonStyle::Filled, None)
};
(style, tooltip)
}
None => (ButtonStyle::Filled, None),
};
let provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
/// Whether or not we should allow messages to be sent.
/// Will return false if the selected provided has a configuration error or
/// if the user has not accepted the terms of service for this provider.
fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
let model = LanguageModelRegistry::read_global(cx).default_model();
let has_configuration_error = configuration_error(cx).is_some();
let needs_to_accept_terms = self.show_accept_terms
&& provider
&& model
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx));
let disabled = has_configuration_error || needs_to_accept_terms;
ButtonLike::new("edit_button")
.disabled(disabled)
.style(style)
.when_some(tooltip, |button, tooltip| {
button.tooltip(move |_, _| tooltip.clone())
})
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Suggest Edits"))
.children(
KeyBinding::for_action_in(&Edit, &focus_handle, window, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, window, cx| {
focus_handle.dispatch_action(&Edit, window, cx);
})
.map_or(false, |model| model.provider.must_accept_terms(cx));
has_configuration_error || needs_to_accept_terms
}
fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -2598,7 +2088,6 @@ impl ContextEditor {
.elevation_2(cx)
.occlude()
.child(match last_error {
AssistError::FileRequired => self.render_file_required_error(cx),
AssistError::PaymentRequired => self.render_payment_required_error(cx),
AssistError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
@@ -2611,41 +2100,6 @@ impl ContextEditor {
)
}
fn render_file_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::Warning).color(Color::Warning))
.child(
Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(
"To include files, type /file or /tab in your prompt.",
)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, _window, cx| {
this.last_error = None;
cx.notify();
},
))),
)
.into_any()
}
fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
@@ -3086,7 +2540,6 @@ impl Render for ContextEditor {
.capture_action(cx.listener(ContextEditor::paste))
.capture_action(cx.listener(ContextEditor::cycle_message_role))
.capture_action(cx.listener(ContextEditor::confirm_command))
.on_action(cx.listener(ContextEditor::edit))
.on_action(cx.listener(ContextEditor::assist))
.on_action(cx.listener(ContextEditor::split))
.on_action(move |_: &ToggleModelSelector, window, cx| {
@@ -3139,20 +2592,6 @@ impl Render for ContextEditor {
h_flex()
.w_full()
.justify_end()
.when(
AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
|buttons| {
buttons
.items_center()
.gap_1p5()
.child(self.render_edit_button(window, cx))
.child(
Label::new("or")
.size(LabelSize::Small)
.color(Color::Muted),
)
},
)
.child(self.render_send_button(window, cx)),
),
),
@@ -3359,10 +2798,10 @@ impl FollowableItem for ContextEditor {
let editor_state = state.editor?;
let project = workspace.read(cx).project().clone();
let assistant_panel_delegate = <dyn AssistantPanelDelegate>::try_global(cx)?;
let agent_panel_delegate = <dyn AgentPanelDelegate>::try_global(cx)?;
let context_editor_task = workspace.update(cx, |workspace, cx| {
assistant_panel_delegate.open_remote_context(workspace, context_id, window, cx)
agent_panel_delegate.open_remote_context(workspace, context_id, window, cx)
});
Some(window.spawn(cx, async move |cx| {

View File

@@ -8,7 +8,7 @@ use ui::{Avatar, ListItem, ListItemSpacing, prelude::*};
use workspace::{Item, Workspace};
use crate::{
AssistantPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
AgentPanelDelegate, ContextStore, DEFAULT_TAB_TITLE, RemoteContextMetadata,
SavedContextMetadata,
};
@@ -70,19 +70,19 @@ impl ContextHistory {
) {
let SavedContextPickerEvent::Confirmed(context) = event;
let Some(assistant_panel_delegate) = <dyn AssistantPanelDelegate>::try_global(cx) else {
let Some(agent_panel_delegate) = <dyn AgentPanelDelegate>::try_global(cx) else {
return;
};
self.workspace
.update(cx, |workspace, cx| match context {
ContextMetadata::Remote(metadata) => {
assistant_panel_delegate
agent_panel_delegate
.open_remote_context(workspace, metadata.id.clone(), window, cx)
.detach_and_log_err(cx);
}
ContextMetadata::Saved(metadata) => {
assistant_panel_delegate
agent_panel_delegate
.open_saved_context(workspace, metadata.path.clone(), window, cx)
.detach_and_log_err(cx);
}

View File

@@ -1,957 +0,0 @@
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use editor::ProposedChangesEditor;
use futures::{TryFutureExt as _, future};
use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString};
use language::{AutoindentMode, Buffer, BufferSnapshot};
use project::{Project, ProjectPath};
use std::{cmp, ops::Range, path::Path, sync::Arc};
use text::{AnchorRangeExt as _, Bias, OffsetRangeExt as _, Point};
#[derive(Clone, Debug)]
pub struct AssistantPatch {
pub range: Range<language::Anchor>,
pub title: SharedString,
pub edits: Arc<[Result<AssistantEdit>]>,
pub status: AssistantPatchStatus,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum AssistantPatchStatus {
Pending,
Ready,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssistantEdit {
pub path: String,
pub kind: AssistantEditKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AssistantEditKind {
Update {
old_text: String,
new_text: String,
description: Option<String>,
},
Create {
new_text: String,
description: Option<String>,
},
InsertBefore {
old_text: String,
new_text: String,
description: Option<String>,
},
InsertAfter {
old_text: String,
new_text: String,
description: Option<String>,
},
Delete {
old_text: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedPatch {
pub edit_groups: HashMap<Entity<Buffer>, Vec<ResolvedEditGroup>>,
pub errors: Vec<AssistantPatchResolutionError>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedEditGroup {
pub context_range: Range<language::Anchor>,
pub edits: Vec<ResolvedEdit>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedEdit {
range: Range<language::Anchor>,
new_text: String,
description: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssistantPatchResolutionError {
pub edit_ix: usize,
pub message: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum SearchDirection {
Up,
Left,
Diagonal,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct SearchState {
cost: u32,
direction: SearchDirection,
}
impl SearchState {
fn new(cost: u32, direction: SearchDirection) -> Self {
Self { cost, direction }
}
}
struct SearchMatrix {
cols: usize,
data: Vec<SearchState>,
}
impl SearchMatrix {
fn new(rows: usize, cols: usize) -> Self {
SearchMatrix {
cols,
data: vec![SearchState::new(0, SearchDirection::Diagonal); rows * cols],
}
}
fn get(&self, row: usize, col: usize) -> SearchState {
self.data[row * self.cols + col]
}
fn set(&mut self, row: usize, col: usize, cost: SearchState) {
self.data[row * self.cols + col] = cost;
}
}
impl ResolvedPatch {
pub fn apply(&self, editor: &ProposedChangesEditor, cx: &mut App) {
for (buffer, groups) in &self.edit_groups {
let branch = editor.branch_buffer_for_base(buffer).unwrap();
Self::apply_edit_groups(groups, &branch, cx);
}
editor.recalculate_all_buffer_diffs();
}
fn apply_edit_groups(groups: &Vec<ResolvedEditGroup>, buffer: &Entity<Buffer>, cx: &mut App) {
let mut edits = Vec::new();
for group in groups {
for suggestion in &group.edits {
edits.push((suggestion.range.clone(), suggestion.new_text.clone()));
}
}
buffer.update(cx, |buffer, cx| {
buffer.edit(
edits,
Some(AutoindentMode::Block {
original_indent_columns: Vec::new(),
}),
cx,
);
});
}
}
impl ResolvedEdit {
pub fn try_merge(&mut self, other: &Self, buffer: &text::BufferSnapshot) -> bool {
let range = &self.range;
let other_range = &other.range;
// Don't merge if we don't contain the other suggestion.
if range.start.cmp(&other_range.start, buffer).is_gt()
|| range.end.cmp(&other_range.end, buffer).is_lt()
{
return false;
}
let other_offset_range = other_range.to_offset(buffer);
let offset_range = range.to_offset(buffer);
// If the other range is empty at the start of this edit's range, combine the new text
if other_offset_range.is_empty() && other_offset_range.start == offset_range.start {
self.new_text = format!("{}\n{}", other.new_text, self.new_text);
self.range.start = other_range.start;
if let Some((description, other_description)) =
self.description.as_mut().zip(other.description.as_ref())
{
*description = format!("{}\n{}", other_description, description)
}
} else {
if let Some((description, other_description)) =
self.description.as_mut().zip(other.description.as_ref())
{
description.push('\n');
description.push_str(other_description);
}
}
true
}
}
impl AssistantEdit {
pub fn new(
path: Option<String>,
operation: Option<String>,
old_text: Option<String>,
new_text: Option<String>,
description: Option<String>,
) -> Result<Self> {
let path = path.ok_or_else(|| anyhow!("missing path"))?;
let operation = operation.ok_or_else(|| anyhow!("missing operation"))?;
let kind = match operation.as_str() {
"update" => AssistantEditKind::Update {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description,
},
"insert_before" => AssistantEditKind::InsertBefore {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description,
},
"insert_after" => AssistantEditKind::InsertAfter {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
description,
},
"delete" => AssistantEditKind::Delete {
old_text: old_text.ok_or_else(|| anyhow!("missing old_text"))?,
},
"create" => AssistantEditKind::Create {
description,
new_text: new_text.ok_or_else(|| anyhow!("missing new_text"))?,
},
_ => Err(anyhow!("unknown operation {operation:?}"))?,
};
Ok(Self { path, kind })
}
pub async fn resolve(
&self,
project: Entity<Project>,
mut cx: AsyncApp,
) -> Result<(Entity<Buffer>, ResolvedEdit)> {
let path = self.path.clone();
let kind = self.kind.clone();
let buffer = project
.update(&mut cx, |project, cx| {
let project_path = project
.find_project_path(Path::new(&path), cx)
.or_else(|| {
// If we couldn't find a project path for it, put it in the active worktree
// so that when we create the buffer, it can be saved.
let worktree = project
.active_entry()
.and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
.or_else(|| project.worktrees(cx).next())?;
let worktree = worktree.read(cx);
Some(ProjectPath {
worktree_id: worktree.id(),
path: Arc::from(Path::new(&path)),
})
})
.with_context(|| format!("worktree not found for {:?}", path))?;
anyhow::Ok(project.open_buffer(project_path, cx))
})??
.await?;
let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
let suggestion = cx
.background_spawn(async move { kind.resolve(&snapshot) })
.await;
Ok((buffer, suggestion))
}
}
impl AssistantEditKind {
fn resolve(self, snapshot: &BufferSnapshot) -> ResolvedEdit {
match self {
Self::Update {
old_text,
new_text,
description,
} => {
let range = Self::resolve_location(&snapshot, &old_text);
ResolvedEdit {
range,
new_text,
description,
}
}
Self::Create {
new_text,
description,
} => ResolvedEdit {
range: text::Anchor::MIN..text::Anchor::MAX,
description,
new_text,
},
Self::InsertBefore {
old_text,
mut new_text,
description,
} => {
let range = Self::resolve_location(&snapshot, &old_text);
new_text.push('\n');
ResolvedEdit {
range: range.start..range.start,
new_text,
description,
}
}
Self::InsertAfter {
old_text,
mut new_text,
description,
} => {
let range = Self::resolve_location(&snapshot, &old_text);
new_text.insert(0, '\n');
ResolvedEdit {
range: range.end..range.end,
new_text,
description,
}
}
Self::Delete { old_text } => {
let range = Self::resolve_location(&snapshot, &old_text);
ResolvedEdit {
range,
new_text: String::new(),
description: None,
}
}
}
}
fn resolve_location(buffer: &text::BufferSnapshot, search_query: &str) -> Range<text::Anchor> {
const INSERTION_COST: u32 = 3;
const DELETION_COST: u32 = 10;
const WHITESPACE_INSERTION_COST: u32 = 1;
const WHITESPACE_DELETION_COST: u32 = 1;
let buffer_len = buffer.len();
let query_len = search_query.len();
let mut matrix = SearchMatrix::new(query_len + 1, buffer_len + 1);
let mut leading_deletion_cost = 0_u32;
for (row, query_byte) in search_query.bytes().enumerate() {
let deletion_cost = if query_byte.is_ascii_whitespace() {
WHITESPACE_DELETION_COST
} else {
DELETION_COST
};
leading_deletion_cost = leading_deletion_cost.saturating_add(deletion_cost);
matrix.set(
row + 1,
0,
SearchState::new(leading_deletion_cost, SearchDirection::Diagonal),
);
for (col, buffer_byte) in buffer.bytes_in_range(0..buffer.len()).flatten().enumerate() {
let insertion_cost = if buffer_byte.is_ascii_whitespace() {
WHITESPACE_INSERTION_COST
} else {
INSERTION_COST
};
let up = SearchState::new(
matrix.get(row, col + 1).cost.saturating_add(deletion_cost),
SearchDirection::Up,
);
let left = SearchState::new(
matrix.get(row + 1, col).cost.saturating_add(insertion_cost),
SearchDirection::Left,
);
let diagonal = SearchState::new(
if query_byte == *buffer_byte {
matrix.get(row, col).cost
} else {
matrix
.get(row, col)
.cost
.saturating_add(deletion_cost + insertion_cost)
},
SearchDirection::Diagonal,
);
matrix.set(row + 1, col + 1, up.min(left).min(diagonal));
}
}
// Traceback to find the best match
let mut best_buffer_end = buffer_len;
let mut best_cost = u32::MAX;
for col in 1..=buffer_len {
let cost = matrix.get(query_len, col).cost;
if cost < best_cost {
best_cost = cost;
best_buffer_end = col;
}
}
let mut query_ix = query_len;
let mut buffer_ix = best_buffer_end;
while query_ix > 0 && buffer_ix > 0 {
let current = matrix.get(query_ix, buffer_ix);
match current.direction {
SearchDirection::Diagonal => {
query_ix -= 1;
buffer_ix -= 1;
}
SearchDirection::Up => {
query_ix -= 1;
}
SearchDirection::Left => {
buffer_ix -= 1;
}
}
}
let mut start = buffer.offset_to_point(buffer.clip_offset(buffer_ix, Bias::Left));
start.column = 0;
let mut end = buffer.offset_to_point(buffer.clip_offset(best_buffer_end, Bias::Right));
if end.column > 0 {
end.column = buffer.line_len(end.row);
}
buffer.anchor_after(start)..buffer.anchor_before(end)
}
}
impl AssistantPatch {
pub async fn resolve(&self, project: Entity<Project>, cx: &mut AsyncApp) -> ResolvedPatch {
let mut resolve_tasks = Vec::new();
for (ix, edit) in self.edits.iter().enumerate() {
if let Ok(edit) = edit.as_ref() {
resolve_tasks.push(
edit.resolve(project.clone(), cx.clone())
.map_err(move |error| (ix, error)),
);
}
}
let edits = future::join_all(resolve_tasks).await;
let mut errors = Vec::new();
let mut edits_by_buffer = HashMap::default();
for entry in edits {
match entry {
Ok((buffer, edit)) => {
edits_by_buffer
.entry(buffer)
.or_insert_with(Vec::new)
.push(edit);
}
Err((edit_ix, error)) => errors.push(AssistantPatchResolutionError {
edit_ix,
message: error.to_string(),
}),
}
}
// Expand the context ranges of each edit and group edits with overlapping context ranges.
let mut edit_groups_by_buffer = HashMap::default();
for (buffer, edits) in edits_by_buffer {
if let Ok(snapshot) = buffer.update(cx, |buffer, _| buffer.text_snapshot()) {
edit_groups_by_buffer.insert(buffer, Self::group_edits(edits, &snapshot));
}
}
ResolvedPatch {
edit_groups: edit_groups_by_buffer,
errors,
}
}
fn group_edits(
mut edits: Vec<ResolvedEdit>,
snapshot: &text::BufferSnapshot,
) -> Vec<ResolvedEditGroup> {
let mut edit_groups = Vec::<ResolvedEditGroup>::new();
// Sort edits by their range so that earlier, larger ranges come first
edits.sort_by(|a, b| a.range.cmp(&b.range, &snapshot));
// Merge overlapping edits
edits.dedup_by(|a, b| b.try_merge(a, &snapshot));
// Create context ranges for each edit
for edit in edits {
let context_range = {
let edit_point_range = edit.range.to_point(&snapshot);
let start_row = edit_point_range.start.row.saturating_sub(5);
let end_row = cmp::min(edit_point_range.end.row + 5, snapshot.max_point().row);
let start = snapshot.anchor_before(Point::new(start_row, 0));
let end = snapshot.anchor_after(Point::new(end_row, snapshot.line_len(end_row)));
start..end
};
if let Some(last_group) = edit_groups.last_mut() {
if last_group
.context_range
.end
.cmp(&context_range.start, &snapshot)
.is_ge()
{
// Merge with the previous group if context ranges overlap
last_group.context_range.end = context_range.end;
last_group.edits.push(edit);
} else {
// Create a new group
edit_groups.push(ResolvedEditGroup {
context_range,
edits: vec![edit],
});
}
} else {
// Create the first group
edit_groups.push(ResolvedEditGroup {
context_range,
edits: vec![edit],
});
}
}
edit_groups
}
pub fn path_count(&self) -> usize {
self.paths().count()
}
pub fn paths(&self) -> impl '_ + Iterator<Item = &str> {
let mut prev_path = None;
self.edits.iter().filter_map(move |edit| {
if let Ok(edit) = edit {
let path = Some(edit.path.as_str());
if path != prev_path {
prev_path = path;
return path;
}
}
None
})
}
}
impl PartialEq for AssistantPatch {
fn eq(&self, other: &Self) -> bool {
self.range == other.range
&& self.title == other.title
&& Arc::ptr_eq(&self.edits, &other.edits)
}
}
impl Eq for AssistantPatch {}
#[cfg(test)]
mod tests {
use super::*;
use gpui::App;
use language::{
Language, LanguageConfig, LanguageMatcher, language_settings::AllLanguageSettings,
};
use settings::SettingsStore;
use ui::BorrowAppContext;
use unindent::Unindent as _;
use util::test::{generate_marked_text, marked_text_ranges};
#[gpui::test]
fn test_resolve_location(cx: &mut App) {
assert_location_resolution(
concat!(
" Lorem\n",
"« ipsum\n",
" dolor sit amet»\n",
" consecteur",
),
"ipsum\ndolor",
cx,
);
assert_location_resolution(
&"
«fn foo1(a: usize) -> usize {
40
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"fn foo1(b: usize) {\n40\n}",
cx,
);
assert_location_resolution(
&"
fn main() {
« Foo
.bar()
.baz()
.qux()»
}
fn foo2(b: usize) -> usize {
42
}
"
.unindent(),
"Foo.bar.baz.qux()",
cx,
);
assert_location_resolution(
&"
class Something {
one() { return 1; }
« two() { return 2222; }
three() { return 333; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
» seven() { return 7; }
eight() { return 8; }
}
"
.unindent(),
&"
two() { return 2222; }
four() { return 4444; }
five() { return 5555; }
six() { return 6666; }
"
.unindent(),
cx,
);
}
#[gpui::test]
fn test_resolve_edits(cx: &mut App) {
init_test(cx);
assert_edits(
"
/// A person
struct Person {
name: String,
age: usize,
}
/// A dog
struct Dog {
weight: f32,
}
impl Person {
fn name(&self) -> &str {
&self.name
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "
name: String,
"
.unindent(),
new_text: "
first_name: String,
last_name: String,
"
.unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "
fn name(&self) -> &str {
&self.name
}
"
.unindent(),
new_text: "
fn name(&self) -> String {
format!(\"{} {}\", self.first_name, self.last_name)
}
"
.unindent(),
description: None,
},
],
"
/// A person
struct Person {
first_name: String,
last_name: String,
age: usize,
}
/// A dog
struct Dog {
weight: f32,
}
impl Person {
fn name(&self) -> String {
format!(\"{} {}\", self.first_name, self.last_name)
}
}
"
.unindent(),
cx,
);
// Ensure InsertBefore merges correctly with Update of the same text
assert_edits(
"
fn foo() {
}
"
.unindent(),
vec![
AssistantEditKind::InsertBefore {
old_text: "
fn foo() {"
.unindent(),
new_text: "
fn bar() {
qux();
}"
.unindent(),
description: Some("implement bar".into()),
},
AssistantEditKind::Update {
old_text: "
fn foo() {
}"
.unindent(),
new_text: "
fn foo() {
bar();
}"
.unindent(),
description: Some("call bar in foo".into()),
},
AssistantEditKind::InsertAfter {
old_text: "
fn foo() {
}
"
.unindent(),
new_text: "
fn qux() {
// todo
}
"
.unindent(),
description: Some("implement qux".into()),
},
],
"
fn bar() {
qux();
}
fn foo() {
bar();
}
fn qux() {
// todo
}
"
.unindent(),
cx,
);
// Correctly indent new text when replacing multiple adjacent indented blocks.
assert_edits(
"
impl Numbers {
fn one() {
1
}
fn two() {
2
}
fn three() {
3
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "
fn one() {
1
}
"
.unindent(),
new_text: "
fn one() {
101
}
"
.unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "
fn two() {
2
}
"
.unindent(),
new_text: "
fn two() {
102
}
"
.unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "
fn three() {
3
}
"
.unindent(),
new_text: "
fn three() {
103
}
"
.unindent(),
description: None,
},
],
"
impl Numbers {
fn one() {
101
}
fn two() {
102
}
fn three() {
103
}
}
"
.unindent(),
cx,
);
assert_edits(
"
impl Person {
fn set_name(&mut self, name: String) {
self.name = name;
}
fn name(&self) -> String {
return self.name;
}
}
"
.unindent(),
vec![
AssistantEditKind::Update {
old_text: "self.name = name;".unindent(),
new_text: "self._name = name;".unindent(),
description: None,
},
AssistantEditKind::Update {
old_text: "return self.name;\n".unindent(),
new_text: "return self._name;\n".unindent(),
description: None,
},
],
"
impl Person {
fn set_name(&mut self, name: String) {
self._name = name;
}
fn name(&self) -> String {
return self._name;
}
}
"
.unindent(),
cx,
);
}
fn init_test(cx: &mut App) {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
cx.update_global::<SettingsStore, _>(|settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
}
#[track_caller]
fn assert_location_resolution(text_with_expected_range: &str, query: &str, cx: &mut App) {
let (text, _) = marked_text_ranges(text_with_expected_range, false);
let buffer = cx.new(|cx| Buffer::local(text.clone(), cx));
let snapshot = buffer.read(cx).snapshot();
let range = AssistantEditKind::resolve_location(&snapshot, query).to_offset(&snapshot);
let text_with_actual_range = generate_marked_text(&text, &[range], false);
pretty_assertions::assert_eq!(text_with_actual_range, text_with_expected_range);
}
#[track_caller]
fn assert_edits(
old_text: String,
edits: Vec<AssistantEditKind>,
new_text: String,
cx: &mut App,
) {
let buffer =
cx.new(|cx| Buffer::local(old_text, cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.read(cx).snapshot();
let resolved_edits = edits
.into_iter()
.map(|kind| kind.resolve(&snapshot))
.collect();
let edit_groups = AssistantPatch::group_edits(resolved_edits, &snapshot);
ResolvedPatch::apply_edit_groups(&edit_groups, &buffer, cx);
let actual_new_text = buffer.read(cx).text();
pretty_assertions::assert_eq!(actual_new_text, new_text);
}
fn rust_lang() -> Language {
Language::new(
LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
..Default::default()
},
Some(language::tree_sitter_rust::LANGUAGE.into()),
)
.with_indents_query(
r#"
(call_expression) @indent
(field_expression) @indent
(_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent
"#,
)
.unwrap()
}
}

View File

@@ -85,7 +85,6 @@ pub struct AssistantSettings {
pub thread_summary_model: Option<LanguageModelSelection>,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub default_profile: AgentProfileId,
pub profiles: IndexMap<AgentProfileId, AgentProfile>,
pub always_allow_tool_actions: bool,
@@ -106,15 +105,6 @@ impl AssistantSettings {
.and_then(|m| m.temperature)
}
pub fn stream_edits(&self, _cx: &App) -> bool {
// TODO: Remove the `stream_edits` setting.
true
}
pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool {
false
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
self.inline_assistant_model = Some(LanguageModelSelection {
provider: provider.into(),
@@ -262,7 +252,6 @@ impl AssistantSettingsContent {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
@@ -293,7 +282,6 @@ impl AssistantSettingsContent {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
@@ -366,7 +354,7 @@ impl AssistantSettingsContent {
&model,
None,
None,
language_model.supports_tools(),
Some(language_model.supports_tools()),
)),
api_url,
});
@@ -575,7 +563,6 @@ impl Default for VersionedAssistantSettingsContent {
commit_message_model: None,
thread_summary_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,
@@ -620,10 +607,6 @@ pub struct AssistantSettingsContentV2 {
thread_summary_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
/// Enable experimental live diffs in the assistant panel.
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
/// The default profile to use in the Agent.
///
/// Default: write
@@ -850,10 +833,6 @@ impl Settings for AssistantSettings {
.thread_summary_model
.or(settings.thread_summary_model.take());
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
merge(
&mut settings.always_allow_tool_actions,
value.always_allow_tool_actions,
@@ -999,7 +978,6 @@ mod tests {
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
default_profile: None,
profiles: None,
always_allow_tool_actions: None,

View File

@@ -7,6 +7,7 @@ mod tool_working_set;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use anyhow::Result;
@@ -17,7 +18,8 @@ use gpui::IntoElement;
use gpui::Window;
use gpui::{App, Entity, SharedString, Task, WeakEntity};
use icons::IconName;
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModel;
use language_model::LanguageModelRequest;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use workspace::Workspace;
@@ -61,11 +63,34 @@ impl ToolUseStatus {
}
}
#[derive(Debug)]
pub struct ToolResultOutput {
pub content: String,
pub output: Option<serde_json::Value>,
}
impl From<String> for ToolResultOutput {
fn from(value: String) -> Self {
ToolResultOutput {
content: value,
output: None,
}
}
}
impl Deref for ToolResultOutput {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.content
}
}
/// The result of running a tool, containing both the asynchronous output
/// and an optional card view that can be rendered immediately.
pub struct ToolResult {
/// The asynchronous task that will eventually resolve to the tool's output
pub output: Task<Result<String>>,
pub output: Task<Result<ToolResultOutput>>,
/// An optional view to present the output of the tool.
pub card: Option<AnyToolCard>,
}
@@ -128,9 +153,9 @@ impl AnyToolCard {
}
}
impl From<Task<Result<String>>> for ToolResult {
impl From<Task<Result<ToolResultOutput>>> for ToolResult {
/// Convert from a task to a ToolResult with no card
fn from(output: Task<Result<String>>) -> Self {
fn from(output: Task<Result<ToolResultOutput>>) -> Self {
Self { output, card: None }
}
}
@@ -181,12 +206,23 @@ pub trait Tool: 'static + Send + Sync {
fn run(
self: Arc<Self>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult;
fn deserialize_card(
self: Arc<Self>,
_output: serde_json::Value,
_project: Entity<Project>,
_window: &mut Window,
_cx: &mut App,
) -> Option<AnyToolCard> {
None
}
}
impl Debug for dyn Tool {

View File

@@ -35,25 +35,17 @@ fn adapt_to_json_schema_subset(json: &mut Value) -> Result<()> {
}
}
const KEYS_TO_REMOVE: [&str; 4] = [
const KEYS_TO_REMOVE: [&str; 5] = [
"format",
"additionalProperties",
"exclusiveMinimum",
"exclusiveMaximum",
"optional",
];
for key in KEYS_TO_REMOVE {
obj.remove(key);
}
if let Some(default) = obj.get("default") {
let is_null = default.is_null();
// Default is not supported, so we need to remove it
obj.remove("default");
if is_null {
obj.insert("nullable".to_string(), Value::Bool(true));
}
}
// If a type is not specified for an input parameter, add a default type
if matches!(obj.get("description"), Some(Value::String(_)))
&& !obj.contains_key("type")
@@ -92,26 +84,6 @@ mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_transform_default_null_to_nullable() {
let mut json = json!({
"description": "A test field",
"type": "string",
"default": null
});
adapt_to_json_schema_subset(&mut json).unwrap();
assert_eq!(
json,
json!({
"description": "A test field",
"type": "string",
"nullable": true
})
);
}
#[test]
fn test_transform_adds_type_when_missing() {
let mut json = json!({
@@ -157,7 +129,8 @@ mod tests {
"format": "uint32",
"exclusiveMinimum": 0,
"exclusiveMaximum": 100,
"additionalProperties": false
"additionalProperties": false,
"optional": true
});
adapt_to_json_schema_subset(&mut json).unwrap();

View File

@@ -17,14 +17,14 @@ eval = []
[dependencies]
aho-corasick.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true
assistant_settings.workspace = true
assistant_tool.workspace = true
buffer_diff.workspace = true
chrono.workspace = true
collections.workspace = true
component.workspace = true
editor.workspace = true
derive_more.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
@@ -35,8 +35,9 @@ indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
log.workspace = true
linkme.workspace = true
log.workspace = true
markdown.workspace = true
open.workspace = true
paths.workspace = true
portable-pty.workspace = true

View File

@@ -1,6 +1,5 @@
mod copy_path_tool;
mod create_directory_tool;
mod create_file_tool;
mod delete_path_tool;
mod diagnostics_tool;
mod edit_agent;
@@ -13,9 +12,7 @@ mod move_path_tool;
mod now_tool;
mod open_tool;
mod read_file_tool;
mod replace;
mod schema;
mod streaming_edit_file_tool;
mod templates;
mod terminal_tool;
mod thinking_tool;
@@ -24,14 +21,12 @@ mod web_search_tool;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use gpui::{App, Entity};
use http_client::HttpClientWithUrl;
use language_model::LanguageModelRegistry;
use move_path_tool::MovePathTool;
use settings::{Settings, SettingsStore};
use web_search_tool::WebSearchTool;
pub(crate) use templates::*;
@@ -39,21 +34,18 @@ pub(crate) use templates::*;
use crate::create_directory_tool::CreateDirectoryTool;
use crate::delete_path_tool::DeletePathTool;
use crate::diagnostics_tool::DiagnosticsTool;
use crate::edit_file_tool::EditFileTool;
use crate::fetch_tool::FetchTool;
use crate::find_path_tool::FindPathTool;
use crate::grep_tool::GrepTool;
use crate::list_directory_tool::ListDirectoryTool;
use crate::now_tool::NowTool;
use crate::read_file_tool::ReadFileTool;
use crate::streaming_edit_file_tool::StreamingEditFileTool;
use crate::thinking_tool::ThinkingTool;
pub use create_file_tool::{CreateFileTool, CreateFileToolInput};
pub use edit_file_tool::{EditFileTool, EditFileToolInput};
pub use edit_file_tool::EditFileToolInput;
pub use find_path_tool::FindPathToolInput;
pub use open_tool::OpenTool;
pub use read_file_tool::ReadFileToolInput;
pub use streaming_edit_file_tool::StreamingEditFileToolInput;
pub use read_file_tool::{ReadFileTool, ReadFileToolInput};
pub use terminal_tool::TerminalTool;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
@@ -74,10 +66,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
registry.register_tool(GrepTool);
registry.register_tool(ThinkingTool);
registry.register_tool(FetchTool::new(http_client));
register_edit_file_tool(cx);
cx.observe_global::<SettingsStore>(register_edit_file_tool)
.detach();
registry.register_tool(EditFileTool);
register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
cx.subscribe(
@@ -104,29 +93,16 @@ fn register_web_search_tool(registry: &Entity<LanguageModelRegistry>, cx: &mut A
}
}
fn register_edit_file_tool(cx: &mut App) {
let registry = ToolRegistry::global(cx);
registry.unregister_tool(CreateFileTool);
registry.unregister_tool(EditFileTool);
registry.unregister_tool(StreamingEditFileTool);
if AssistantSettings::get_global(cx).stream_edits(cx) {
registry.register_tool(StreamingEditFileTool);
} else {
registry.register_tool(CreateFileTool);
registry.register_tool(EditFileTool);
}
}
#[cfg(test)]
mod tests {
use super::*;
use assistant_settings::AssistantSettings;
use client::Client;
use clock::FakeSystemClock;
use http_client::FakeHttpClient;
use schemars::JsonSchema;
use serde::Serialize;
use settings::Settings;
#[test]
fn test_json_schema() {

View File

@@ -3,8 +3,8 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, AppContext, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use language_model::LanguageModel;
use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -74,9 +74,10 @@ impl Tool for CopyPathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -107,10 +108,9 @@ impl Tool for CopyPathTool {
cx.background_spawn(async move {
match copy_task.await {
Ok(_) => Ok(format!(
"Copied {} to {}",
input.source_path, input.destination_path
)),
Ok(_) => Ok(
format!("Copied {} to {}", input.source_path, input.destination_path).into(),
),
Err(err) => Err(anyhow!(
"Failed to copy {} to {}: {}",
input.source_path,

View File

@@ -3,8 +3,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -62,9 +61,10 @@ impl Tool for CreateDirectoryTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -88,7 +88,7 @@ impl Tool for CreateDirectoryTool {
.await
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
Ok(format!("Created directory {destination_path}"))
Ok(format!("Created directory {destination_path}").into())
})
.into()
}

View File

@@ -1,195 +0,0 @@
use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::AnyWindowHandle;
use gpui::{App, Entity, Task};
use language_model::LanguageModelRequestMessage;
use language_model::LanguageModelToolSchemaFormat;
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use ui::IconName;
use util::markdown::MarkdownInlineCode;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CreateFileToolInput {
/// The path where the file should be created.
///
/// <example>
/// If the project has the following structure:
///
/// - directory1/
/// - directory2/
///
/// You can create a new file by providing a path of "directory1/new_file.txt"
/// </example>
///
/// Make sure to include this field before the `contents` field in the input object
/// so that we can display it immediately.
pub path: String,
/// The text contents of the file to create.
///
/// <example>
/// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
/// </example>
pub contents: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PartialInput {
#[serde(default)]
path: String,
#[serde(default)]
contents: String,
}
pub struct CreateFileTool;
const DEFAULT_UI_TEXT: &str = "Create file";
impl Tool for CreateFileTool {
fn name(&self) -> String {
"create_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("./create_file_tool/description.md").into()
}
fn icon(&self) -> IconName {
IconName::FileCreate
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<CreateFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
Ok(input) => {
let path = MarkdownInlineCode(&input.path);
format!("Create file {path}")
}
Err(_) => DEFAULT_UI_TEXT.to_string(),
}
}
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<PartialInput>(input.clone()).ok() {
Some(input) if !input.path.is_empty() => input.path,
_ => DEFAULT_UI_TEXT.to_string(),
}
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<CreateFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
Some(project_path) => project_path,
None => {
return Task::ready(Err(anyhow!("Path to create was outside the project"))).into();
}
};
let contents: Arc<str> = input.contents.as_str().into();
let destination_path: Arc<str> = input.path.as_str().into();
cx.spawn(async move |cx| {
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await
.map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
cx.update(|cx| {
action_log.update(cx, |action_log, cx| {
action_log.buffer_created(buffer.clone(), cx)
});
buffer.update(cx, |buffer, cx| buffer.set_text(contents, cx));
action_log.update(cx, |action_log, cx| {
action_log.buffer_edited(buffer.clone(), cx)
});
})?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.await
.map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
Ok(format!("Created file {destination_path}"))
})
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn still_streaming_ui_text_with_path() {
let tool = CreateFileTool;
let input = json!({
"path": "src/main.rs",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
}
#[test]
fn still_streaming_ui_text_without_path() {
let tool = CreateFileTool;
let input = json!({
"path": "",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
}
#[test]
fn still_streaming_ui_text_with_null() {
let tool = CreateFileTool;
let input = serde_json::Value::Null;
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
}
#[test]
fn ui_text_with_valid_input() {
let tool = CreateFileTool;
let input = json!({
"path": "src/main.rs",
"contents": "fn main() {\n println!(\"Hello, world!\");\n}"
});
assert_eq!(tool.ui_text(&input), "Create file `src/main.rs`");
}
#[test]
fn ui_text_with_invalid_input() {
let tool = CreateFileTool;
let input = json!({
"invalid": "field"
});
assert_eq!(tool.ui_text(&input), DEFAULT_UI_TEXT);
}
}

View File

@@ -1,3 +0,0 @@
Creates a new file at the specified path within the project, containing the given text content. Returns confirmation that the file was created.
This tool is the most efficient way to create new files within the project, so it should always be chosen whenever it's necessary to create a new file in the project with specific text content, or whenever a file in the project needs such a drastic change that you would prefer to replace the entire thing instead of making individual edits. This tool should not be used when making changes to parts of an existing file but not all of it. In those cases, it's better to use another approach to edit the file.

View File

@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::{SinkExt, StreamExt, channel::mpsc};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{Project, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -59,9 +59,10 @@ impl Tool for DeletePathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -127,7 +128,7 @@ impl Tool for DeletePathTool {
match delete {
Some(deletion_task) => match deletion_task.await {
Ok(()) => Ok(format!("Deleted {path_str}")),
Ok(()) => Ok(format!("Deleted {path_str}").into()),
Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
},
None => Err(anyhow!(

View File

@@ -3,7 +3,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{DiagnosticSeverity, OffsetRangeExt};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -79,9 +79,10 @@ impl Tool for DiagnosticsTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -122,9 +123,9 @@ impl Tool for DiagnosticsTool {
}
if output.is_empty() {
Ok("File doesn't have errors or warnings!".to_string())
Ok("File doesn't have errors or warnings!".to_string().into())
} else {
Ok(output)
Ok(output.into())
}
})
.into()
@@ -158,10 +159,12 @@ impl Tool for DiagnosticsTool {
});
if has_diagnostics {
Task::ready(Ok(output)).into()
Task::ready(Ok(output.into())).into()
} else {
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
.into()
Task::ready(Ok("No errors or warnings found in the project."
.to_string()
.into()))
.into()
}
}
}

View File

@@ -17,10 +17,11 @@ use gpui::{AppContext, AsyncApp, Entity, SharedString, Task};
use language::{Bias, Buffer, BufferSnapshot, LineIndent, Point};
use language_model::{
LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage,
MessageContent, Role,
LanguageModelToolChoice, MessageContent, Role,
};
use project::{AgentLocation, Project};
use serde::Serialize;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{cmp, iter, mem, ops::Range, path::PathBuf, sync::Arc, task::Poll};
use streaming_diff::{CharOperation, StreamingDiff};
@@ -50,10 +51,10 @@ pub enum EditAgentOutputEvent {
OldTextNotFound(SharedString),
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditAgentOutput {
pub _raw_edits: String,
pub _parser_metrics: EditParserMetrics,
pub raw_edits: String,
pub parser_metrics: EditParserMetrics,
}
#[derive(Clone)]
@@ -83,7 +84,7 @@ impl EditAgent {
&self,
buffer: Entity<Buffer>,
edit_description: String,
previous_messages: Vec<LanguageModelRequestMessage>,
conversation: &LanguageModelRequest,
cx: &mut AsyncApp,
) -> (
Task<Result<EditAgentOutput>>,
@@ -91,6 +92,7 @@ impl EditAgent {
) {
let this = self.clone();
let (events_tx, events_rx) = mpsc::unbounded();
let conversation = conversation.clone();
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
@@ -99,7 +101,7 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let new_chunks = this.request(previous_messages, prompt, cx).await?;
let new_chunks = this.request(conversation, prompt, cx).await?;
let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -185,8 +187,8 @@ impl EditAgent {
}
Ok(EditAgentOutput {
_raw_edits: raw_edits,
_parser_metrics: EditParserMetrics::default(),
raw_edits,
parser_metrics: EditParserMetrics::default(),
})
}
@@ -194,7 +196,7 @@ impl EditAgent {
&self,
buffer: Entity<Buffer>,
edit_description: String,
previous_messages: Vec<LanguageModelRequestMessage>,
conversation: &LanguageModelRequest,
cx: &mut AsyncApp,
) -> (
Task<Result<EditAgentOutput>>,
@@ -214,6 +216,7 @@ impl EditAgent {
let this = self.clone();
let (events_tx, events_rx) = mpsc::unbounded();
let conversation = conversation.clone();
let output = cx.spawn(async move |cx| {
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
let path = cx.update(|cx| snapshot.resolve_file_path(cx, true))?;
@@ -222,7 +225,7 @@ impl EditAgent {
edit_description,
}
.render(&this.templates)?;
let edit_chunks = this.request(previous_messages, prompt, cx).await?;
let edit_chunks = this.request(conversation, prompt, cx).await?;
let (output, mut inner_events) = this.apply_edit_chunks(buffer, edit_chunks, cx);
while let Some(event) = inner_events.next().await {
@@ -424,8 +427,8 @@ impl EditAgent {
}
}
Ok(EditAgentOutput {
_raw_edits: raw_edits,
_parser_metrics: parser.finish(),
raw_edits,
parser_metrics: parser.finish(),
})
});
(output, rx)
@@ -512,32 +515,67 @@ impl EditAgent {
async fn request(
&self,
mut messages: Vec<LanguageModelRequestMessage>,
mut conversation: LanguageModelRequest,
prompt: String,
cx: &mut AsyncApp,
) -> Result<BoxStream<'static, Result<String, LanguageModelCompletionError>>> {
let mut message_content = Vec::new();
if let Some(last_message) = messages.last_mut() {
let mut messages_iter = conversation.messages.iter_mut();
if let Some(last_message) = messages_iter.next_back() {
if last_message.role == Role::Assistant {
let old_content_len = last_message.content.len();
last_message
.content
.retain(|content| !matches!(content, MessageContent::ToolUse(_)));
let new_content_len = last_message.content.len();
// We just removed pending tool uses from the content of the
// last message, so it doesn't make sense to cache it anymore
// (e.g., the message will look very different on the next
// request). Thus, we move the flag to the message prior to it,
// as it will still be a valid prefix of the conversation.
if old_content_len != new_content_len && last_message.cache {
if let Some(prev_message) = messages_iter.next_back() {
last_message.cache = false;
prev_message.cache = true;
}
}
if last_message.content.is_empty() {
messages.pop();
conversation.messages.pop();
}
}
}
message_content.push(MessageContent::Text(prompt));
messages.push(LanguageModelRequestMessage {
conversation.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: message_content,
content: vec![MessageContent::Text(prompt)],
cache: false,
});
// Include tools in the request so that we can take advantage of
// caching when ToolChoice::None is supported.
let mut tool_choice = None;
let mut tools = Vec::new();
if !conversation.tools.is_empty()
&& self
.model
.supports_tool_choice(LanguageModelToolChoice::None)
{
tool_choice = Some(LanguageModelToolChoice::None);
tools = conversation.tools.clone();
}
let request = LanguageModelRequest {
messages,
..Default::default()
thread_id: conversation.thread_id,
prompt_id: conversation.prompt_id,
mode: conversation.mode,
messages: conversation.messages,
tool_choice,
tools,
stop: Vec::new(),
temperature: None,
};
Ok(self.model.stream_completion_text(request, cx).await?.stream)
}

View File

@@ -1,4 +1,6 @@
use derive_more::{Add, AddAssign};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::{cmp, mem, ops::Range};
@@ -13,7 +15,9 @@ pub enum EditParserEvent {
NewTextChunk { chunk: String, done: bool },
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Add, AddAssign)]
#[derive(
Clone, Debug, Default, PartialEq, Eq, Add, AddAssign, Serialize, Deserialize, JsonSchema,
)]
pub struct EditParserMetrics {
pub tags: usize,
pub mismatched_tags: usize,

View File

@@ -1,18 +1,17 @@
use super::*;
use crate::{
ReadFileToolInput, grep_tool::GrepToolInput,
streaming_edit_file_tool::StreamingEditFileToolInput,
};
use crate::{ReadFileToolInput, edit_file_tool::EditFileToolInput, grep_tool::GrepToolInput};
use Role::*;
use anyhow::anyhow;
use assistant_tool::ToolRegistry;
use client::{Client, UserStore};
use collections::HashMap;
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext};
use indoc::indoc;
use indoc::{formatdoc, indoc};
use language_model::{
LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId,
LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId,
};
use project::Project;
use rand::prelude::*;
@@ -40,7 +39,7 @@ fn eval_extract_handle_command_output() {
conversation: vec![
message(
User,
[text(indoc! {"
[text(formatdoc! {"
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
@@ -69,7 +68,7 @@ fn eval_extract_handle_command_output() {
[tool_use(
"tool_2",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -99,7 +98,7 @@ fn eval_delete_run_git_blame() {
conversation: vec![
message(
User,
[text(indoc! {"
[text(formatdoc! {"
Read the `{input_file_path}` file and delete `run_git_blame`. Just that
one function, not its usages.
"})],
@@ -125,7 +124,7 @@ fn eval_delete_run_git_blame() {
[tool_use(
"tool_2",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -141,6 +140,61 @@ fn eval_delete_run_git_blame() {
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_translate_doc_comments() {
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
let edit_description = "Translate all doc comments to Italian";
eval(
200,
1.,
EvalInput {
conversation: vec![
message(
User,
[text(formatdoc! {"
Read the {input_file_path} file and edit it (without overwriting it),
translating all the doc comments to italian.
"})],
),
message(
Assistant,
[tool_use(
"tool_1",
"read_file",
ReadFileToolInput {
path: input_file_path.into(),
start_line: None,
end_line: None,
},
)],
),
message(
User,
[tool_result("tool_1", "read_file", input_file_content)],
),
message(
Assistant,
[tool_use(
"tool_2",
"edit_file",
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
},
)],
),
],
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::judge_diff("Doc comments were translated to Italian"),
},
);
}
#[test]
#[cfg_attr(not(feature = "eval"), ignore)]
fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
@@ -155,7 +209,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
conversation: vec![
message(
User,
[text(indoc! {"
[text(formatdoc! {"
Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
@@ -163,7 +217,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{language_name}
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}}
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
@@ -240,7 +294,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
[tool_use(
"tool_4",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -264,11 +318,10 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
fn eval_disable_cursor_blinking() {
let input_file_path = "root/editor.rs";
let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs");
let output_file_content = include_str!("evals/fixtures/disable_cursor_blinking/after.rs");
let edit_description = "Comment out the call to `BlinkManager::enable`";
eval(
200,
0.6, // TODO: make this eval better
0.95,
EvalInput {
conversation: vec![
message(User, [text("Let's research how to cursor blinking works.")]),
@@ -316,7 +369,7 @@ fn eval_disable_cursor_blinking() {
[tool_use(
"tool_4",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -327,7 +380,11 @@ fn eval_disable_cursor_blinking() {
input_path: input_file_path.into(),
input_content: Some(input_file_content.into()),
edit_description: edit_description.into(),
assertion: EvalAssertion::assert_eq(output_file_content),
assertion: EvalAssertion::judge_diff(indoc! {"
- Calls to BlinkManager in `observe_window_activation` were commented out
- The call to `blink_manager.enable` above the call to show_cursor_names was commented out
- All the edits have valid indentation
"}),
},
);
}
@@ -506,7 +563,7 @@ fn eval_from_pixels_constructor() {
[tool_use(
"tool_5",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -583,7 +640,7 @@ fn eval_zode() {
tool_use(
"tool_3",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: true,
@@ -828,7 +885,7 @@ fn eval_add_overwrite_test() {
tool_use(
"tool_5",
"edit_file",
StreamingEditFileToolInput {
EditFileToolInput {
display_description: edit_description.into(),
path: input_file_path.into(),
create_or_overwrite: false,
@@ -895,6 +952,7 @@ fn tool_result(
tool_name: name.into(),
is_error: false,
content: result.into(),
output: None,
})
}
@@ -1033,7 +1091,8 @@ impl EvalAssertion {
fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
let mut evaluated_count = 0;
report_progress(evaluated_count, iterations);
let mut failed_count = 0;
report_progress(evaluated_count, failed_count, iterations);
let (tx, rx) = mpsc::channel();
@@ -1050,7 +1109,6 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
}
drop(tx);
let mut failed_count = 0;
let mut failed_evals = HashMap::default();
let mut errored_evals = HashMap::default();
let mut eval_outputs = Vec::new();
@@ -1058,7 +1116,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
while let Ok(output) = rx.recv() {
match output {
Ok(output) => {
cumulative_parser_metrics += output.sample.edit_output._parser_metrics.clone();
cumulative_parser_metrics += output.sample.edit_output.parser_metrics.clone();
eval_outputs.push(output.clone());
if output.assertion.score < 80 {
failed_count += 1;
@@ -1075,7 +1133,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
}
evaluated_count += 1;
report_progress(evaluated_count, iterations);
report_progress(evaluated_count, failed_count, iterations);
}
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
@@ -1139,15 +1197,26 @@ impl Display for EvalOutput {
writeln!(
f,
"Parser Metrics:\n{:#?}",
self.sample.edit_output._parser_metrics
self.sample.edit_output.parser_metrics
)?;
writeln!(f, "Raw Edits:\n{}", self.sample.edit_output._raw_edits)?;
writeln!(f, "Raw Edits:\n{}", self.sample.edit_output.raw_edits)?;
Ok(())
}
}
fn report_progress(evaluated_count: usize, iterations: usize) {
print!("\r\x1b[KEvaluated {}/{}", evaluated_count, iterations);
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let passed_count = evaluated_count - failed_count;
let passed_ratio = if evaluated_count == 0 {
0.0
} else {
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}%)",
evaluated_count,
iterations,
passed_ratio * 100.0
);
std::io::stdout().flush().unwrap();
}
@@ -1160,25 +1229,30 @@ struct EditAgentTest {
impl EditAgentTest {
async fn new(cx: &mut TestAppContext) -> Self {
cx.executor().allow_parking();
cx.update(settings::init);
cx.update(Project::init_settings);
cx.update(language::init);
cx.update(gpui_tokio::init);
cx.update(client::init_settings);
let fs = FakeFs::new(cx.executor().clone());
cx.update(|cx| {
settings::init(cx);
gpui_tokio::init(cx);
let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap());
cx.set_http_client(http_client);
client::init_settings(cx);
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
settings::init(cx);
Project::init_settings(cx);
language::init(cx);
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
crate::init(client.http_client(), cx);
});
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let (agent_model, judge_model) = cx
.update(|cx| {
let http_client = ReqwestClient::user_agent("agent tests").unwrap();
cx.set_http_client(Arc::new(http_client));
let client = Client::production(cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
cx.spawn(async move |cx| {
let agent_model =
Self::load_model("anthropic", "claude-3-7-sonnet-latest", cx).await;
@@ -1227,12 +1301,32 @@ impl EditAgentTest {
.update(cx, |project, cx| project.open_buffer(path, cx))
.await
.unwrap();
let conversation = LanguageModelRequest {
messages: eval.conversation,
tools: cx.update(|cx| {
ToolRegistry::default_global(cx)
.tools()
.into_iter()
.filter_map(|tool| {
let input_schema = tool
.input_schema(self.agent.model.tool_input_format())
.ok()?;
Some(LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema,
})
})
.collect()
}),
..Default::default()
};
let edit_output = if let Some(input_content) = eval.input_content.as_deref() {
buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
let (edit_output, _) = self.agent.edit(
buffer.clone(),
eval.edit_description,
eval.conversation,
&conversation,
&mut cx.to_async(),
);
edit_output.await?
@@ -1240,7 +1334,7 @@ impl EditAgentTest {
let (edit_output, _) = self.agent.overwrite(
buffer.clone(),
eval.edit_description,
eval.conversation,
&conversation,
&mut cx.to_async(),
);
edit_output.await?

View File

@@ -0,0 +1,339 @@
// font-kit/src/canvas.rs
//
// Copyright © 2018 The Pathfinder Project Developers.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//! An in-memory bitmap surface for glyph rasterization.
use lazy_static::lazy_static;
use pathfinder_geometry::rect::RectI;
use pathfinder_geometry::vector::Vector2I;
use std::cmp;
use std::fmt;
use crate::utils;
lazy_static! {
static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = {
let mut lut = [[0; 8]; 256];
for byte in 0..0x100 {
let mut value = [0; 8];
for bit in 0..8 {
if (byte & (0x80 >> bit)) != 0 {
value[bit] = 0xff;
}
}
lut[byte] = value
}
lut
};
}
/// An in-memory bitmap surface for glyph rasterization.
pub struct Canvas {
/// The raw pixel data.
pub pixels: Vec<u8>,
/// The size of the buffer, in pixels.
pub size: Vector2I,
/// The number of *bytes* between successive rows.
pub stride: usize,
/// The image format of the canvas.
pub format: Format,
}
impl Canvas {
/// Creates a new blank canvas with the given pixel size and format.
///
/// Stride is automatically calculated from width.
///
/// The canvas is initialized with transparent black (all values 0).
#[inline]
pub fn new(size: Vector2I, format: Format) -> Canvas {
Canvas::with_stride(
size,
size.x() as usize * format.bytes_per_pixel() as usize,
format,
)
}
/// Creates a new blank canvas with the given pixel size, stride (number of bytes between
/// successive rows), and format.
///
/// The canvas is initialized with transparent black (all values 0).
pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas {
Canvas {
pixels: vec![0; stride * size.y() as usize],
size,
stride,
format,
}
}
#[allow(dead_code)]
pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) {
self.blit_from(
Vector2I::default(),
&src.pixels,
src.size,
src.stride,
src.format,
)
}
/// Blits to a rectangle with origin at `dst_point` and size according to `src_size`.
/// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted.
/// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes.
/// `src_stride` must be equal or larger than the actual data length.
#[allow(dead_code)]
pub(crate) fn blit_from(
&mut self,
dst_point: Vector2I,
src_bytes: &[u8],
src_size: Vector2I,
src_stride: usize,
src_format: Format,
) {
assert_eq!(
src_stride * src_size.y() as usize,
src_bytes.len(),
"Number of pixels in src_bytes does not match stride and size."
);
assert!(
src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize,
"src_stride must be >= than src_size.x()"
);
let dst_rect = RectI::new(dst_point, src_size);
let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size));
let dst_rect = match dst_rect {
Some(dst_rect) => dst_rect,
None => return,
};
match (self.format, src_format) {
(Format::A8, Format::A8)
| (Format::Rgb24, Format::Rgb24)
| (Format::Rgba32, Format::Rgba32) => {
self.blit_from_with::<BlitMemcpy>(dst_rect, src_bytes, src_stride, src_format)
}
(Format::A8, Format::Rgb24) => {
self.blit_from_with::<BlitRgb24ToA8>(dst_rect, src_bytes, src_stride, src_format)
}
(Format::Rgb24, Format::A8) => {
self.blit_from_with::<BlitA8ToRgb24>(dst_rect, src_bytes, src_stride, src_format)
}
(Format::Rgb24, Format::Rgba32) => self
.blit_from_with::<BlitRgba32ToRgb24>(dst_rect, src_bytes, src_stride, src_format),
(Format::Rgba32, Format::Rgb24) => self
.blit_from_with::<BlitRgb24ToRgba32>(dst_rect, src_bytes, src_stride, src_format),
(Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(),
}
}
#[allow(dead_code)]
pub(crate) fn blit_from_bitmap_1bpp(
&mut self,
dst_point: Vector2I,
src_bytes: &[u8],
src_size: Vector2I,
src_stride: usize,
) {
if self.format != Format::A8 {
unimplemented!()
}
let dst_rect = RectI::new(dst_point, src_size);
let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size));
let dst_rect = match dst_rect {
Some(dst_rect) => dst_rect,
None => return,
};
let size = dst_rect.size();
let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize;
let dest_row_stride = size.x() as usize * dest_bytes_per_pixel;
let src_row_stride = utils::div_round_up(size.x() as usize, 8);
for y in 0..size.y() {
let (dest_row_start, src_row_start) = (
(y + dst_rect.origin_y()) as usize * self.stride
+ dst_rect.origin_x() as usize * dest_bytes_per_pixel,
y as usize * src_stride,
);
let dest_row_end = dest_row_start + dest_row_stride;
let src_row_end = src_row_start + src_row_stride;
let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end];
let src_row_pixels = &src_bytes[src_row_start..src_row_end];
for x in 0..src_row_stride {
let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize];
let dest_start = x * 8;
let dest_end = cmp::min(dest_start + 8, dest_row_stride);
let src = &pattern[0..(dest_end - dest_start)];
dest_row_pixels[dest_start..dest_end].clone_from_slice(src);
}
}
}
/// Blits to area `rect` using the data given in the buffer `src_bytes`.
/// `src_stride` must be specified in bytes.
/// The dimensions of `rect` must be in pixels.
fn blit_from_with<B: Blit>(
&mut self,
rect: RectI,
src_bytes: &[u8],
src_stride: usize,
src_format: Format,
) {
let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize;
let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize;
for y in 0..rect.height() {
let (dest_row_start, src_row_start) = (
(y + rect.origin_y()) as usize * self.stride
+ rect.origin_x() as usize * dest_bytes_per_pixel,
y as usize * src_stride,
);
let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel;
let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel;
let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end];
let src_row_pixels = &src_bytes[src_row_start..src_row_end];
B::blit(dest_row_pixels, src_row_pixels)
}
}
}
impl fmt::Debug for Canvas {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Canvas")
.field("pixels", &self.pixels.len()) // Do not dump a vector content.
.field("size", &self.size)
.field("stride", &self.stride)
.field("format", &self.format)
.finish()
}
}
/// The image format for the canvas.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Format {
/// Premultiplied R8G8B8A8, little-endian.
Rgba32,
/// R8G8B8, little-endian.
Rgb24,
/// A8.
A8,
}
impl Format {
/// Returns the number of bits per pixel that this image format corresponds to.
#[inline]
pub fn bits_per_pixel(self) -> u8 {
match self {
Format::Rgba32 => 32,
Format::Rgb24 => 24,
Format::A8 => 8,
}
}
/// Returns the number of color channels per pixel that this image format corresponds to.
#[inline]
pub fn components_per_pixel(self) -> u8 {
match self {
Format::Rgba32 => 4,
Format::Rgb24 => 3,
Format::A8 => 1,
}
}
/// Returns the number of bits per color channel that this image format contains.
#[inline]
pub fn bits_per_component(self) -> u8 {
self.bits_per_pixel() / self.components_per_pixel()
}
/// Returns the number of bytes per pixel that this image format corresponds to.
#[inline]
pub fn bytes_per_pixel(self) -> u8 {
self.bits_per_pixel() / 8
}
}
/// The antialiasing strategy that should be used when rasterizing glyphs.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RasterizationOptions {
/// "Black-and-white" rendering. Each pixel is either entirely on or off.
Bilevel,
/// Grayscale antialiasing. Only one channel is used.
GrayscaleAa,
/// Subpixel RGB antialiasing, for LCD screens.
SubpixelAa,
}
trait Blit {
fn blit(dest: &mut [u8], src: &[u8]);
}
struct BlitMemcpy;
impl Blit for BlitMemcpy {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
dest.clone_from_slice(src)
}
}
struct BlitRgb24ToA8;
impl Blit for BlitRgb24ToA8 {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
// TODO(pcwalton): SIMD.
for (dest, src) in dest.iter_mut().zip(src.chunks(3)) {
*dest = src[1]
}
}
}
struct BlitA8ToRgb24;
impl Blit for BlitA8ToRgb24 {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
for (dest, src) in dest.chunks_mut(3).zip(src.iter()) {
dest[0] = *src;
dest[1] = *src;
dest[2] = *src;
}
}
}
struct BlitRgba32ToRgb24;
impl Blit for BlitRgba32ToRgb24 {
#[inline]
fn blit(dest: &mut [u8], src: &[u8]) {
// TODO(pcwalton): SIMD.
for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) {
dest.copy_from_slice(&src[0..3])
}
}
}
struct BlitRgb24ToRgba32;
impl Blit for BlitRgb24ToRgba32 {
fn blit(dest: &mut [u8], src: &[u8]) {
for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) {
dest[0] = src[0];
dest[1] = src[1];
dest[2] = src[2];
dest[3] = 255;
}
}
}

View File

@@ -1,21 +1,27 @@
use crate::{
replace::{replace_exact, replace_with_flexible_indent},
Templates,
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolUseStatus};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorElement, EditorMode, EditorStyle, MultiBuffer, PathKey};
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EntityId,
Task, TextStyle, WeakEntity, pulsating_between,
use anyhow::{Result, anyhow};
use assistant_tool::{
ActionLog, AnyToolCard, Tool, ToolCard, ToolResult, ToolResultOutput, ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, EntityId, Task,
TextStyleRefinement, WeakEntity, pulsating_between,
};
use indoc::formatdoc;
use language::{
Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Rope, TextBuffer,
language_settings::SoftWrap,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::{AgentLocation, Project};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@@ -25,7 +31,7 @@ use std::{
time::Duration,
};
use theme::ThemeSettings;
use ui::{Disclosure, Tooltip, Window, prelude::*};
use ui::{Disclosure, Tooltip, prelude::*};
use util::ResultExt;
use workspace::Workspace;
@@ -33,7 +39,13 @@ pub struct EditFileTool;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
///
/// Be terse, but also descriptive in what you want to achieve with this
/// edit. Avoid generic instructions.
///
/// NEVER mention the file path in this description.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
@@ -42,7 +54,7 @@ pub struct EditFileToolInput {
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to modify in the project.
/// The full path of the file to create or modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
@@ -63,11 +75,20 @@ pub struct EditFileToolInput {
/// </example>
pub path: PathBuf,
/// The text to replace.
pub old_string: String,
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
///
/// When a file already exists or you just created it, always prefer editing
/// it as opposed to recreating it from scratch.
pub create_or_overwrite: bool,
}
/// The text to replace it with.
pub new_string: String,
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolOutput {
pub original_path: PathBuf,
pub new_text: String,
pub old_text: String,
pub raw_output: Option<EditAgentOutput>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
@@ -76,10 +97,6 @@ struct PartialInput {
path: String,
#[serde(default)]
display_description: String,
#[serde(default)]
old_string: String,
#[serde(default)]
new_string: String,
}
const DEFAULT_UI_TEXT: &str = "Editing file";
@@ -131,9 +148,10 @@ impl Tool for EditFileTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -142,6 +160,14 @@ impl Tool for EditFileTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
};
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
@@ -154,11 +180,7 @@ impl Tool for EditFileTool {
let card_clone = card.clone();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
let project_path = project.read_with(cx, |project, cx| {
project
.find_project_path(&input.path, cx)
.context("Path not found in project")
})??;
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
let buffer = project
.update(cx, |project, cx| {
@@ -166,118 +188,89 @@ impl Tool for EditFileTool {
})?
.await?;
// Set the agent's location to the top of the file
project
.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: language::Anchor::MIN,
}),
cx,
);
})
.ok();
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
if input.old_string.is_empty() {
return Err(anyhow!(
"`old_string` can't be empty, use another tool if you want to create a file."
));
let exists = buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(false, |file| file.disk_state().exists())
})?;
if !input.create_or_overwrite && !exists {
return Err(anyhow!("{} not found", input.path.display()));
}
if input.old_string == input.new_string {
return Err(anyhow!(
"The `old_string` and `new_string` are identical, so no changes would be made."
));
}
let result = cx
.background_spawn(async move {
// Try to match exactly
let diff = replace_exact(&input.old_string, &input.new_string, &snapshot)
.await
// If that fails, try being flexible about indentation
.or_else(|| {
replace_with_flexible_indent(
&input.old_string,
&input.new_string,
&snapshot,
)
})?;
if diff.edits.is_empty() {
return None;
}
let old_text = snapshot.text();
Some((old_text, diff))
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
let old_snapshot = old_snapshot.clone();
async move { old_snapshot.text() }
})
.await;
let Some((old_text, diff)) = result else {
let err = buffer.read_with(cx, |buffer, _cx| {
let file_exists = buffer
.file()
.map_or(false, |file| file.disk_state().exists());
if !file_exists {
anyhow!("{} does not exist", input.path.display())
} else if buffer.is_empty() {
anyhow!(
"{} is empty, so the provided `old_string` wasn't found.",
input.path.display()
)
} else {
anyhow!("Failed to match the provided `old_string`")
}
})?;
return Err(err);
let (output, mut events) = if input.create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
} else {
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
&request,
cx,
)
};
let snapshot = cx.update(|cx| {
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
let base_version = diff.base_version.clone();
let snapshot = buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction();
buffer.apply_diff(diff, cx);
buffer.finalize_last_transaction();
buffer.snapshot()
});
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
// Set the agent's location to the position of the first edit
if let Some(first_edit) = snapshot.edits_since::<usize>(&base_version).next() {
let position = snapshot.anchor_before(first_edit.new.start);
project.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position,
}),
cx,
);
})
let mut hallucinated_old_text = false;
while let Some(event) = events.next().await {
match event {
EditAgentOutputEvent::Edited => {
if let Some(card) = card_clone.as_ref() {
let new_snapshot =
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx
.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
})
.await;
card.update(cx, |card, cx| {
card.set_diff(
project_path.path.clone(),
old_text.clone(),
new_text,
cx,
);
})
.log_err();
}
}
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
}
snapshot
})?;
}
let agent_output = output.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
let new_text = snapshot.text();
let diff_str = cx
.background_spawn({
let old_text = old_text.clone();
let new_text = new_text.clone();
async move { language::unified_diff(&old_text, &new_text) }
})
.await;
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
});
let diff = cx.background_spawn(async move {
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
});
let (new_text, diff) = futures::join!(new_text, diff);
let output = EditFileToolOutput {
original_path: project_path.path.to_path_buf(),
new_text: new_text.clone(),
old_text: old_text.clone(),
raw_output: Some(agent_output),
};
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
@@ -286,11 +279,23 @@ impl Tool for EditFileTool {
.log_err();
}
Ok(format!(
"Edited {}:\n\n```diff\n{}\n```",
input.path.display(),
diff_str
))
let input_path = input.path.display();
if diff.is_empty() {
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string().into())
}
} else {
Ok(ToolResultOutput {
content: format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff),
output: serde_json::to_value(output).ok(),
})
}
});
ToolResult {
@@ -298,6 +303,32 @@ impl Tool for EditFileTool {
card: card.map(AnyToolCard::from),
}
}
fn deserialize_card(
self: Arc<Self>,
output: serde_json::Value,
project: Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Option<AnyToolCard> {
let output = match serde_json::from_value::<EditFileToolOutput>(output) {
Ok(output) => output,
Err(_) => return None,
};
let card = cx.new(|cx| {
let mut card = EditFileToolCard::new(output.original_path.clone(), project, window, cx);
card.set_diff(
output.original_path.into(),
output.old_text,
output.new_text,
cx,
);
card
});
Some(card.into())
}
}
pub struct EditFileToolCard {
@@ -307,7 +338,7 @@ pub struct EditFileToolCard {
project: Entity<Project>,
diff_task: Option<Task<Result<()>>>,
preview_expanded: bool,
error_expanded: bool,
error_expanded: Option<Entity<Markdown>>,
full_height_expanded: bool,
total_lines: Option<u32>,
editor_unique_id: EntityId,
@@ -331,9 +362,9 @@ impl EditFileToolCard {
editor.set_show_gutter(false, cx);
editor.disable_inline_diagnostics();
editor.disable_expand_excerpt_buttons(cx);
editor.disable_scrollbars_and_minimap(window, cx);
editor.set_soft_wrap_mode(SoftWrap::None, cx);
editor.scroll_manager.set_forbid_vertical_scroll(true);
editor.set_show_scrollbars(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_read_only(true);
editor.set_show_breakpoints(false, cx);
@@ -350,7 +381,7 @@ impl EditFileToolCard {
multibuffer,
diff_task: None,
preview_expanded: true,
error_expanded: false,
error_expanded: None,
full_height_expanded: false,
total_lines: None,
}
@@ -407,9 +438,9 @@ impl ToolCard for EditFileToolCard {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> impl IntoElement {
let (failed, error_message) = match status {
ToolUseStatus::Error(err) => (true, Some(err.to_string())),
_ => (false, None),
let error_message = match status {
ToolUseStatus::Error(err) => Some(err),
_ => None,
};
let path_label_button = h_flex()
@@ -497,9 +528,11 @@ impl ToolCard for EditFileToolCard {
.gap_1()
.justify_between()
.rounded_t_md()
.when(!failed, |header| header.bg(codeblock_header_bg))
.when(error_message.is_none(), |header| {
header.bg(codeblock_header_bg)
})
.child(path_label_button)
.when(failed, |header| {
.when_some(error_message, |header, error_message| {
header.child(
h_flex()
.gap_1()
@@ -511,19 +544,28 @@ impl ToolCard for EditFileToolCard {
.child(
Disclosure::new(
("edit-file-error-disclosure", self.editor_unique_id),
self.error_expanded,
self.error_expanded.is_some(),
)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener(
move |this, _event, _window, _cx| {
this.error_expanded = !this.error_expanded;
},
)),
.on_click(cx.listener({
let error_message = error_message.clone();
move |this, _event, _window, cx| {
if this.error_expanded.is_some() {
this.error_expanded.take();
} else {
this.error_expanded = Some(cx.new(|cx| {
Markdown::new(error_message.clone(), None, None, cx)
}))
}
cx.notify();
}
})),
),
)
})
.when(!failed && self.has_diff(), |header| {
.when(error_message.is_none() && self.has_diff(), |header| {
header.child(
Disclosure::new(
("edit-file-disclosure", self.editor_unique_id),
@@ -545,33 +587,16 @@ impl ToolCard for EditFileToolCard {
.map(|style| style.text.line_height_in_pixels(window.rem_size()))
.unwrap_or_default();
let settings = ThemeSettings::get_global(cx);
let element = EditorElement::new(
&cx.entity(),
EditorStyle {
background: cx.theme().colors().editor_background,
horizontal_padding: rems(0.25).to_pixels(window.rem_size()),
local_player: cx.theme().players().local(),
text: TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.buffer_font.family.clone(),
font_features: settings.buffer_font.features.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: TextSize::Small
.rems(cx)
.to_pixels(settings.agent_font_size(cx))
.into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
..Default::default()
},
scrollbar_width: EditorElement::SCROLLBAR_WIDTH,
syntax: cx.theme().syntax().clone(),
status: cx.theme().status().clone(),
..Default::default()
},
);
editor.set_text_style_refinement(TextStyleRefinement {
font_size: Some(
TextSize::Small
.rems(cx)
.to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
.into(),
),
..TextStyleRefinement::default()
});
let element = editor.render(window, cx);
(element.into_any_element(), line_height)
});
@@ -647,12 +672,12 @@ impl ToolCard for EditFileToolCard {
v_flex()
.mb_2()
.border_1()
.when(failed, |card| card.border_dashed())
.when(error_message.is_some(), |card| card.border_dashed())
.border_color(border_color)
.rounded_md()
.overflow_hidden()
.child(codeblock_header)
.when(failed && self.error_expanded, |card| {
.when_some(self.error_expanded.as_ref(), |card, error_markdown| {
card.child(
v_flex()
.p_2()
@@ -672,65 +697,81 @@ impl ToolCard for EditFileToolCard {
.rounded_md()
.text_ui_sm(cx)
.bg(cx.theme().colors().editor_background)
.children(
error_message
.map(|error| div().child(error).into_any_element()),
),
.child(MarkdownElement::new(
error_markdown.clone(),
markdown_style(window, cx),
)),
),
)
})
.when(!self.has_diff() && !failed, |card| {
.when(!self.has_diff() && error_message.is_none(), |card| {
card.child(waiting_for_diff)
})
.when(
!failed && self.preview_expanded && self.has_diff(),
|card| {
.when(self.preview_expanded && self.has_diff(), |card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
.border_t_1()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(editor)
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
)
.when(is_collapsible, |card| {
card.child(
v_flex()
.relative()
.h_full()
.when(!self.full_height_expanded, |editor_container| {
editor_container
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
})
.overflow_hidden()
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.child(editor)
.when(
!self.full_height_expanded && is_collapsible,
|editor_container| editor_container.child(gradient_overlay),
),
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
.when(is_collapsible, |card| {
card.child(
h_flex()
.id(("expand-button", self.editor_unique_id))
.flex_none()
.cursor_pointer()
.h_5()
.justify_center()
.border_t_1()
.rounded_b_md()
.border_color(border_color)
.bg(cx.theme().colors().editor_background)
.hover(|style| {
style.bg(cx.theme().colors().element_hover.opacity(0.1))
})
.child(
Icon::new(full_height_icon)
.size(IconSize::Small)
.color(Color::Muted),
)
.tooltip(Tooltip::text(full_height_tooltip_label))
.on_click(cx.listener(move |this, _event, _window, _cx| {
this.full_height_expanded = !this.full_height_expanded;
})),
)
})
},
)
})
})
}
}
fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = TextSize::Default.rems(cx);
let mut text_style = window.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
font_features: Some(theme_settings.ui_font.features.clone()),
font_size: Some(ui_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
..Default::default()
}
}
@@ -810,7 +851,48 @@ async fn build_buffer_diff(
#[cfg(test)]
mod tests {
use super::*;
use fs::FakeFs;
use gpui::TestAppContext;
use language_model::fake_provider::FakeLanguageModel;
use serde_json::json;
use settings::SettingsStore;
use util::path;
#[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = serde_json::to_value(EditFileToolInput {
display_description: "Some edit".into(),
path: "root/nonexistent_file.txt".into(),
create_or_overwrite: false,
})
.unwrap();
Arc::new(EditFileTool)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
assert_eq!(
result.unwrap_err().to_string(),
"root/nonexistent_file.txt not found"
);
}
#[test]
fn still_streaming_ui_text_with_path() {
@@ -878,4 +960,13 @@ mod tests {
DEFAULT_UI_TEXT,
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
}
}

View File

@@ -1,4 +1,4 @@
This is a tool for editing files. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. For larger edits, use the `create_file` tool to overwrite files.
This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
Before using this tool:
@@ -6,40 +6,3 @@ Before using this tool:
2. Verify the directory path is correct (only applicable when creating new files):
- Use the `list_directory` tool to verify the parent directory exists and is the correct location
To make a file edit, provide the following:
1. path: The full path to the file you wish to modify in the project. This path must include the root directory in the project.
2. old_string: The text to replace (must be unique within the file, and must match the file contents exactly, including all whitespace and indentation)
3. new_string: The edited text, which will replace the old_string in the file.
The tool will replace ONE occurrence of old_string with new_string in the specified file.
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
1. UNIQUENESS: The old_string MUST uniquely identify the specific instance you want to change. This means:
- Include AT LEAST 3-5 lines of context BEFORE the change point
- Include AT LEAST 3-5 lines of context AFTER the change point
- Include all whitespace, indentation, and surrounding code exactly as it appears in the file
2. SINGLE INSTANCE: This tool can only change ONE instance at a time. If you need to change multiple instances:
- Make separate calls to this tool for each instance
- Each call must uniquely identify its specific instance using extensive context
3. VERIFICATION: Before using this tool:
- Check how many instances of the target text exist in the file
- If multiple instances exist, gather enough context to uniquely identify each one
- Plan separate tool calls for each instance
WARNING: If you do not follow these requirements:
- The tool will fail if old_string matches multiple locations
- The tool will fail if old_string doesn't match exactly (including whitespace)
- You may change the wrong instance if you don't include enough context
When making edits:
- Ensure the edit results in idiomatic, correct code
- Do not leave the code in a broken state
- Always use fully-qualified project paths (starting with the name of one of the project's root directories)
If you want to create a new file, use the `create_file` tool instead of this tool. Don't pass an empty `old_string`.
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.

View File

@@ -9,7 +9,7 @@ use futures::AsyncReadExt as _;
use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task};
use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
use http_client::{AsyncBody, HttpClientWithUrl};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -142,9 +142,10 @@ impl Tool for FetchTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -166,7 +167,7 @@ impl Tool for FetchTool {
bail!("no textual content found");
}
Ok(text)
Ok(text.into())
})
.into()
}

View File

@@ -7,7 +7,7 @@ use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -73,9 +73,10 @@ impl Tool for FindPathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -98,7 +99,7 @@ impl Tool for FindPathTool {
sender.send(paginated_matches.to_vec()).log_err();
if matches.is_empty() {
Ok("No matches found".to_string())
Ok("No matches found".to_string().into())
} else {
let mut message = format!("Found {} total matches.", matches.len());
if matches.len() > RESULTS_PER_PAGE {
@@ -113,7 +114,7 @@ impl Tool for FindPathTool {
for mat in matches.into_iter().skip(offset).take(RESULTS_PER_PAGE) {
write!(&mut message, "\n{}", mat.display()).unwrap();
}
Ok(message)
Ok(message.into())
}
});

View File

@@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::StreamExt;
use gpui::{AnyWindowHandle, App, Entity, Task};
use language::{OffsetRangeExt, ParseStatus, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{
Project,
search::{SearchQuery, SearchResult},
@@ -96,9 +96,10 @@ impl Tool for GrepTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -260,16 +261,16 @@ impl Tool for GrepTool {
}
if matches_found == 0 {
Ok("No matches found".to_string())
Ok("No matches found".to_string().into())
} else if has_more_matches {
Ok(format!(
"Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}",
input.offset + 1,
input.offset + matches_found,
input.offset + RESULTS_PER_PAGE,
))
).into())
} else {
Ok(format!("Found {matches_found} matches:\n{output}"))
Ok(format!("Found {matches_found} matches:\n{output}").into())
}
}).into()
}
@@ -281,6 +282,7 @@ mod tests {
use assistant_tool::Tool;
use gpui::{AppContext, TestAppContext};
use language::{Language, LanguageConfig, LanguageMatcher};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use settings::SettingsStore;
use unindent::Unindent;
@@ -743,14 +745,16 @@ mod tests {
) -> String {
let tool = Arc::new(GrepTool);
let action_log = cx.new(|_cx| ActionLog::new(project.clone()));
let task = cx.update(|cx| tool.run(input, &[], project, action_log, None, cx));
let model = Arc::new(FakeLanguageModel::default());
let task =
cx.update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx));
match task.output.await {
Ok(result) => {
if cfg!(windows) {
result.replace("root\\", "root/")
result.content.replace("root\\", "root/")
} else {
result
result.content
}
}
Err(e) => panic!("Failed to run grep tool: {}", e),

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -73,9 +73,10 @@ impl Tool for ListDirectoryTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -102,7 +103,7 @@ impl Tool for ListDirectoryTool {
.collect::<Vec<_>>()
.join("\n");
return Task::ready(Ok(output)).into();
return Task::ready(Ok(output.into())).into();
}
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
@@ -134,8 +135,8 @@ impl Tool for ListDirectoryTool {
.unwrap();
}
if output.is_empty() {
return Task::ready(Ok(format!("{} is empty.", input.path))).into();
return Task::ready(Ok(format!("{} is empty.", input.path).into())).into();
}
Task::ready(Ok(output)).into()
Task::ready(Ok(output.into())).into()
}
}

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -86,9 +86,10 @@ impl Tool for MovePathTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -117,10 +118,9 @@ impl Tool for MovePathTool {
cx.background_spawn(async move {
match rename_task.await {
Ok(_) => Ok(format!(
"Moved {} to {}",
input.source_path, input.destination_path
)),
Ok(_) => {
Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into())
}
Err(err) => Err(anyhow!(
"Failed to move {} to {}: {}",
input.source_path,

View File

@@ -5,7 +5,7 @@ use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use chrono::{Local, Utc};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -56,9 +56,10 @@ impl Tool for NowTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
@@ -73,6 +74,6 @@ impl Tool for NowTool {
};
let text = format!("The current datetime is {now}.");
Task::ready(Ok(text)).into()
Task::ready(Ok(text.into())).into()
}
}

View File

@@ -2,7 +2,7 @@ use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -49,9 +49,10 @@ impl Tool for OpenTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -70,7 +71,7 @@ impl Tool for OpenTool {
}
.context("Failed to open URL or file path")?;
Ok(format!("Successfully opened {}", input.path_or_url))
Ok(format!("Successfully opened {}", input.path_or_url).into())
})
.into()
}

View File

@@ -7,7 +7,7 @@ use gpui::{AnyWindowHandle, App, Entity, Task};
use indoc::formatdoc;
use itertools::Itertools;
use language::{Anchor, Point};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::{AgentLocation, Project};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -83,9 +83,10 @@ impl Tool for ReadFileTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -97,27 +98,22 @@ impl Tool for ReadFileTool {
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
};
let exists = worktree.update(cx, |worktree, cx| {
worktree.file_exists(&project_path.path, cx)
});
let file_path = input.path.clone();
cx.spawn(async move |cx| {
if !exists.await? {
return Err(anyhow!("{} not found", file_path));
}
let buffer = cx
.update(|cx| {
project.update(cx, |project, cx| project.open_buffer(project_path, cx))
})?
.await?;
if buffer.read_with(cx, |buffer, _| {
buffer
.file()
.as_ref()
.map_or(true, |file| !file.disk_state().exists())
})? {
return Err(anyhow!("{} not found", file_path));
}
project.update(cx, |project, cx| {
project.set_agent_location(
@@ -145,9 +141,13 @@ impl Tool for ReadFileTool {
let lines = text.split('\n').skip(start_row as usize);
if let Some(end) = input.end_line {
let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
Itertools::intersperse(lines.take(count as usize), "\n").collect()
Itertools::intersperse(lines.take(count as usize), "\n")
.collect::<String>()
.into()
} else {
Itertools::intersperse(lines, "\n").collect()
Itertools::intersperse(lines, "\n")
.collect::<String>()
.into()
}
})?;
@@ -180,19 +180,24 @@ impl Tool for ReadFileTool {
log.buffer_read(buffer, cx);
})?;
Ok(result)
Ok(result.into())
} else {
// File is too big, so return the outline
// and a suggestion to read again with line numbers.
let outline = outline::file_outline(project, file_path, action_log, None, cx).await?;
let outline =
outline::file_outline(project, file_path, action_log, None, cx).await?;
Ok(formatdoc! {"
This file was too big to read all at once. Here is an outline of its symbols:
This file was too big to read all at once.
Here is an outline of its symbols:
{outline}
Using the line numbers in this outline, you can call this tool again while specifying
the start_line and end_line fields to see the implementations of symbols in the outline."
})
Using the line numbers in this outline, you can call this tool again
while specifying the start_line and end_line fields to see the
implementations of symbols in the outline."
}
.into())
}
}
})
@@ -205,6 +210,7 @@ mod test {
use super::*;
use gpui::{AppContext, TestAppContext};
use language::{Language, LanguageConfig, LanguageMatcher};
use language_model::fake_provider::FakeLanguageModel;
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
@@ -218,13 +224,22 @@ mod test {
fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = json!({
"path": "root/nonexistent_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
@@ -248,17 +263,26 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = json!({
"path": "root/small_file.txt"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
assert_eq!(result.unwrap(), "This is a small file content");
assert_eq!(result.unwrap().content, "This is a small file content");
}
#[gpui::test]
@@ -277,6 +301,7 @@ mod test {
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(Arc::new(rust_lang()));
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
@@ -284,13 +309,21 @@ mod test {
"path": "root/large_file.rs"
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
.output
})
.await;
let content = result.unwrap();
assert_eq!(
content.lines().skip(2).take(6).collect::<Vec<_>>(),
content.lines().skip(4).take(6).collect::<Vec<_>>(),
vec![
"struct Test0 [L1-4]",
" a [L2]",
@@ -308,7 +341,15 @@ mod test {
"offset": 1
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
@@ -325,7 +366,7 @@ mod test {
pretty_assertions::assert_eq!(
content
.lines()
.skip(2)
.skip(4)
.take(expected_content.len())
.collect::<Vec<_>>(),
expected_content
@@ -346,6 +387,7 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
let result = cx
.update(|cx| {
let input = json!({
@@ -354,11 +396,19 @@ mod test {
"end_line": 4
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
assert_eq!(result.unwrap().content, "Line 2\nLine 3\nLine 4");
}
#[gpui::test]
@@ -375,6 +425,7 @@ mod test {
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let model = Arc::new(FakeLanguageModel::default());
// start_line of 0 should be treated as 1
let result = cx
@@ -385,11 +436,19 @@ mod test {
"end_line": 2
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
.output
})
.await;
assert_eq!(result.unwrap(), "Line 1\nLine 2");
assert_eq!(result.unwrap().content, "Line 1\nLine 2");
// end_line of 0 should result in at least 1 line
let result = cx
@@ -400,11 +459,19 @@ mod test {
"end_line": 0
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log.clone(), None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
)
.output
})
.await;
assert_eq!(result.unwrap(), "Line 1");
assert_eq!(result.unwrap().content, "Line 1");
// when start_line > end_line, should still return at least 1 line
let result = cx
@@ -415,11 +482,19 @@ mod test {
"end_line": 2
});
Arc::new(ReadFileTool)
.run(input, &[], project.clone(), action_log, None, cx)
.run(
input,
Arc::default(),
project.clone(),
action_log,
model,
None,
cx,
)
.output
})
.await;
assert_eq!(result.unwrap(), "Line 3");
assert_eq!(result.unwrap().content, "Line 3");
}
fn init_test(cx: &mut TestAppContext) {

View File

@@ -1,872 +0,0 @@
use language::{BufferSnapshot, Diff, Point, ToOffset};
use project::search::SearchQuery;
use std::iter;
use util::{ResultExt as _, paths::PathMatcher};
/// Performs an exact string replacement in a buffer, requiring precise character-for-character matching.
/// Uses the search functionality to locate the first occurrence of the exact string.
/// Returns None if no exact match is found in the buffer.
pub async fn replace_exact(old: &str, new: &str, snapshot: &BufferSnapshot) -> Option<Diff> {
let query = SearchQuery::text(
old,
false,
true,
true,
PathMatcher::new(iter::empty::<&str>()).ok()?,
PathMatcher::new(iter::empty::<&str>()).ok()?,
false,
None,
)
.log_err()?;
let matches = query.search(&snapshot, None).await;
if matches.is_empty() {
return None;
}
let edit_range = matches[0].clone();
let diff = language::text_diff(&old, &new);
let edits = diff
.into_iter()
.map(|(old_range, text)| {
let start = edit_range.start + old_range.start;
let end = edit_range.start + old_range.end;
(start..end, text)
})
.collect::<Vec<_>>();
let diff = language::Diff {
base_version: snapshot.version().clone(),
line_ending: snapshot.line_ending(),
edits,
};
Some(diff)
}
/// Performs a replacement that's indentation-aware - matches text content ignoring leading whitespace differences.
/// When replacing, preserves the indentation level found in the buffer at each matching line.
/// Returns None if no match found or if indentation is offset inconsistently across matched lines.
pub fn replace_with_flexible_indent(old: &str, new: &str, buffer: &BufferSnapshot) -> Option<Diff> {
let (old_lines, old_min_indent) = lines_with_min_indent(old);
let (new_lines, new_min_indent) = lines_with_min_indent(new);
let min_indent = old_min_indent.min(new_min_indent);
let old_lines = drop_lines_prefix(&old_lines, min_indent);
let new_lines = drop_lines_prefix(&new_lines, min_indent);
let max_row = buffer.max_point().row;
'windows: for start_row in 0..max_row + 1 {
let end_row = start_row + old_lines.len().saturating_sub(1) as u32;
if end_row > max_row {
// The buffer ends before fully matching the pattern
return None;
}
let start_point = Point::new(start_row, 0);
let end_point = Point::new(end_row, buffer.line_len(end_row));
let range = start_point.to_offset(buffer)..end_point.to_offset(buffer);
let window_text = buffer.text_for_range(range.clone());
let mut window_lines = window_text.lines();
let mut old_lines_iter = old_lines.iter();
let mut common_mismatch = None;
#[derive(Eq, PartialEq)]
enum Mismatch {
OverIndented(String),
UnderIndented(String),
}
while let (Some(window_line), Some(old_line)) = (window_lines.next(), old_lines_iter.next())
{
let line_trimmed = window_line.trim_start();
if line_trimmed != old_line.trim_start() {
continue 'windows;
}
if line_trimmed.is_empty() {
continue;
}
let line_mismatch = if window_line.len() > old_line.len() {
let prefix = window_line[..window_line.len() - old_line.len()].to_string();
Mismatch::UnderIndented(prefix)
} else {
let prefix = old_line[..old_line.len() - window_line.len()].to_string();
Mismatch::OverIndented(prefix)
};
match &common_mismatch {
Some(common_mismatch) if common_mismatch != &line_mismatch => {
continue 'windows;
}
Some(_) => (),
None => common_mismatch = Some(line_mismatch),
}
}
if let Some(common_mismatch) = &common_mismatch {
let line_ending = buffer.line_ending();
let replacement = new_lines
.iter()
.map(|new_line| {
if new_line.trim().is_empty() {
new_line.to_string()
} else {
match common_mismatch {
Mismatch::UnderIndented(prefix) => prefix.to_string() + new_line,
Mismatch::OverIndented(prefix) => new_line
.strip_prefix(prefix)
.unwrap_or(new_line)
.to_string(),
}
}
})
.collect::<Vec<_>>()
.join(line_ending.as_str());
let diff = Diff {
base_version: buffer.version().clone(),
line_ending,
edits: vec![(range, replacement.into())],
};
return Some(diff);
}
}
None
}
fn drop_lines_prefix<'a>(lines: &'a [&str], prefix_len: usize) -> Vec<&'a str> {
lines
.iter()
.map(|line| line.get(prefix_len..).unwrap_or(""))
.collect()
}
fn lines_with_min_indent(input: &str) -> (Vec<&str>, usize) {
let mut lines = Vec::new();
let mut min_indent: Option<usize> = None;
for line in input.lines() {
lines.push(line);
if !line.trim().is_empty() {
let indent = line.len() - line.trim_start().len();
min_indent = Some(min_indent.map_or(indent, |m| m.min(indent)));
}
}
(lines, min_indent.unwrap_or(0))
}
#[cfg(test)]
mod replace_exact_tests {
use super::*;
use gpui::TestAppContext;
use gpui::prelude::*;
#[gpui::test]
async fn basic(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
assert_eq!(result, Some("let x = 42;".to_string()));
}
#[gpui::test]
async fn no_match(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let y = 42;", "let y = 43;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn multi_line(cx: &mut TestAppContext) {
let whole = "fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}";
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
let result = test_replace_exact(cx, whole, old_text, new_text).await;
assert_eq!(
result,
Some("fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}".to_string())
);
}
#[gpui::test]
async fn multiple_occurrences(cx: &mut TestAppContext) {
let whole = "let x = 41;\nlet y = 41;\nlet z = 41;";
let result = test_replace_exact(cx, whole, "let x = 41;", "let x = 42;").await;
assert_eq!(
result,
Some("let x = 42;\nlet y = 41;\nlet z = 41;".to_string())
);
}
#[gpui::test]
async fn empty_buffer(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "", "let x = 41;", "let x = 42;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn partial_match(cx: &mut TestAppContext) {
let whole = "let x = 41; let y = 42;";
let result = test_replace_exact(cx, whole, "let x = 41", "let x = 42").await;
assert_eq!(result, Some("let x = 42; let y = 42;".to_string()));
}
#[gpui::test]
async fn whitespace_sensitive(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", " let x = 41;", "let x = 42;").await;
assert_eq!(result, None);
}
#[gpui::test]
async fn entire_buffer(cx: &mut TestAppContext) {
let result = test_replace_exact(cx, "let x = 41;", "let x = 41;", "let x = 42;").await;
assert_eq!(result, Some("let x = 42;".to_string()));
}
async fn test_replace_exact(
cx: &mut TestAppContext,
whole: &str,
old: &str,
new: &str,
) -> Option<String> {
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact(old, new, &buffer_snapshot).await;
diff.map(|diff| {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
})
})
}
}
#[cfg(test)]
mod flexible_indent_tests {
use super::*;
use gpui::TestAppContext;
use gpui::prelude::*;
use unindent::Unindent;
#[gpui::test]
fn test_underindented_single_line(cx: &mut TestAppContext) {
let cur = " let a = 41;".to_string();
let old = " let a = 41;".to_string();
let new = " let a = 42;".to_string();
let exp = " let a = 42;".to_string();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(exp.to_string()))
}
#[gpui::test]
fn test_overindented_single_line(cx: &mut TestAppContext) {
let cur = " let a = 41;".to_string();
let old = " let a = 41;".to_string();
let new = " let a = 42;".to_string();
let exp = " let a = 42;".to_string();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(exp.to_string()))
}
#[gpui::test]
fn test_underindented_multi_line(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
println!("x = {}", x);
let y = 10;
}
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 42;
println!("New value: {}", x);
"#
.unindent();
let expected = r#"
fn test() {
let x = 42;
println!("New value: {}", x);
let y = 10;
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[gpui::test]
fn test_overindented_multi_line(cx: &mut TestAppContext) {
let cur = r#"
fn foo() {
let a = 41;
let b = 3.13;
}
"#
.unindent();
// 6 space indent instead of 4
let old = " let a = 41;\n let b = 3.13;";
let new = " let a = 42;\n let b = 3.14;";
let expected = r#"
fn foo() {
let a = 42;
let b = 3.14;
}
"#
.unindent();
let result = test_replace_with_flexible_indent(cx, &cur, &old, &new);
assert_eq!(result, Some(expected.to_string()))
}
#[gpui::test]
fn test_replace_inconsistent_indentation(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
if condition {
println!("{}", 43);
}
}
"#
.unindent();
let old = r#"
if condition {
println!("{}", 43);
"#
.unindent();
let new = r#"
if condition {
println!("{}", 42);
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_with_empty_lines(cx: &mut TestAppContext) {
// Test with empty lines
let whole = r#"
fn test() {
let x = 5;
println!("x = {}", x);
}
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 10;
println!("New x: {}", x);
"#
.unindent();
let expected = r#"
fn test() {
let x = 10;
println!("New x: {}", x);
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[gpui::test]
fn test_replace_no_match(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
}
"#
.unindent();
let old = r#"
let y = 10;
"#
.unindent();
let new = r#"
let y = 20;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_ends_before_matching_old(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 10;
println!("x = {}", x);
"#
.unindent();
// Should return None because whole doesn't fully contain the old text
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_is_shorter_than_old(cx: &mut TestAppContext) {
let whole = r#"
let x = 5;
"#
.unindent();
let old = r#"
let x = 5;
let y = 10;
"#
.unindent();
let new = r#"
let x = 5;
let y = 20;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_old_is_empty(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
let x = 5;
}
"#
.unindent();
let old = "";
let new = r#"
let y = 10;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[gpui::test]
fn test_replace_whole_is_empty(cx: &mut TestAppContext) {
let whole = "";
let old = r#"
let x = 5;
"#
.unindent();
let new = r#"
let x = 10;
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
None
);
}
#[test]
fn test_lines_with_min_indent() {
// Empty string
assert_eq!(lines_with_min_indent(""), (vec![], 0));
// Single line without indentation
assert_eq!(lines_with_min_indent("hello"), (vec!["hello"], 0));
// Multiple lines with no indentation
assert_eq!(
lines_with_min_indent("line1\nline2\nline3"),
(vec!["line1", "line2", "line3"], 0)
);
// Multiple lines with consistent indentation
assert_eq!(
lines_with_min_indent(" line1\n line2\n line3"),
(vec![" line1", " line2", " line3"], 2)
);
// Multiple lines with varying indentation
assert_eq!(
lines_with_min_indent(" line1\n line2\n line3"),
(vec![" line1", " line2", " line3"], 2)
);
// Lines with mixed indentation and empty lines
assert_eq!(
lines_with_min_indent(" line1\n\n line2"),
(vec![" line1", "", " line2"], 2)
);
}
#[gpui::test]
fn test_replace_with_missing_indent_uneven_match(cx: &mut TestAppContext) {
let whole = r#"
fn test() {
if true {
let x = 5;
println!("x = {}", x);
}
}
"#
.unindent();
let old = r#"
let x = 5;
println!("x = {}", x);
"#
.unindent();
let new = r#"
let x = 42;
println!("x = {}", x);
"#
.unindent();
let expected = r#"
fn test() {
if true {
let x = 42;
println!("x = {}", x);
}
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[gpui::test]
fn test_replace_big_example(cx: &mut TestAppContext) {
let whole = r#"
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
}
"#
.unindent();
let old = r#"
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
"#
.unindent();
let new = r#"
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
#[test]
fn test_group_people_by_age() {
let people = vec![
Person::new("Young One", 5, "young@example.com").unwrap(),
Person::new("Teen One", 15, "teen@example.com").unwrap(),
Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
Person::new("Adult One", 25, "adult@example.com").unwrap(),
];
let groups = group_people_by_age(&people);
assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9
assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
}
"#
.unindent();
let expected = r#"
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_age() {
assert!(is_valid_age(0));
assert!(!is_valid_age(151));
}
#[test]
fn test_group_people_by_age() {
let people = vec![
Person::new("Young One", 5, "young@example.com").unwrap(),
Person::new("Teen One", 15, "teen@example.com").unwrap(),
Person::new("Teen Two", 18, "teen2@example.com").unwrap(),
Person::new("Adult One", 25, "adult@example.com").unwrap(),
];
let groups = group_people_by_age(&people);
assert_eq!(groups.get(&0).unwrap().len(), 1); // One person in 0-9
assert_eq!(groups.get(&10).unwrap().len(), 2); // Two people in 10-19
assert_eq!(groups.get(&20).unwrap().len(), 1); // One person in 20-29
}
}
"#
.unindent();
assert_eq!(
test_replace_with_flexible_indent(cx, &whole, &old, &new),
Some(expected.to_string())
);
}
#[test]
fn test_drop_lines_prefix() {
// Empty array
assert_eq!(drop_lines_prefix(&[], 2), Vec::<&str>::new());
// Zero prefix length
assert_eq!(
drop_lines_prefix(&["line1", "line2"], 0),
vec!["line1", "line2"]
);
// Normal prefix drop
assert_eq!(
drop_lines_prefix(&[" line1", " line2"], 2),
vec!["line1", "line2"]
);
// Prefix longer than some lines
assert_eq!(drop_lines_prefix(&[" line1", "a"], 2), vec!["line1", ""]);
// Prefix longer than all lines
assert_eq!(drop_lines_prefix(&["a", "b"], 5), vec!["", ""]);
// Mixed length lines
assert_eq!(
drop_lines_prefix(&[" line1", " line2", " line3"], 2),
vec![" line1", "line2", " line3"]
);
}
#[gpui::test]
async fn test_replace_exact_basic(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
assert_eq!(diff.edits.len(), 1);
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;");
}
#[gpui::test]
async fn test_replace_exact_no_match(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let y = 42;", "let y = 43;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_multi_line(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| {
language::Buffer::local(
"fn example() {\n let x = 41;\n println!(\"x = {}\", x);\n}",
cx,
)
});
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let old_text = " let x = 41;\n println!(\"x = {}\", x);";
let new_text = " let x = 42;\n println!(\"x = {}\", x);";
let diff = replace_exact(old_text, new_text, &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(
result,
"fn example() {\n let x = 42;\n println!(\"x = {}\", x);\n}"
);
}
#[gpui::test]
async fn test_replace_exact_multiple_occurrences(cx: &mut TestAppContext) {
let buffer =
cx.new(|cx| language::Buffer::local("let x = 41;\nlet y = 41;\nlet z = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Should replace only the first occurrence
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;\nlet y = 41;\nlet z = 41;");
}
#[gpui::test]
async fn test_replace_exact_empty_buffer(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_partial_match(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41; let y = 42;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Verify substring replacement actually works
let diff = replace_exact("let x = 41", "let x = 42", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42; let y = 42;");
}
#[gpui::test]
async fn test_replace_exact_whitespace_sensitive(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact(" let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_none());
}
#[gpui::test]
async fn test_replace_exact_entire_buffer(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| language::Buffer::local("let x = 41;", cx));
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
let diff = replace_exact("let x = 41;", "let x = 42;", &snapshot).await;
assert!(diff.is_some());
let diff = diff.unwrap();
let result = buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
});
assert_eq!(result, "let x = 42;");
}
fn test_replace_with_flexible_indent(
cx: &mut TestAppContext,
whole: &str,
old: &str,
new: &str,
) -> Option<String> {
// Create a local buffer with the test content
let buffer = cx.new(|cx| language::Buffer::local(whole, cx));
// Get the buffer snapshot
let buffer_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot());
// Call replace_flexible and transform the result
replace_with_flexible_indent(old, new, &buffer_snapshot).map(|diff| {
buffer.update(cx, |buffer, cx| {
let _ = buffer.apply_diff(diff, cx);
buffer.text()
})
})
}
}

View File

@@ -1,355 +0,0 @@
use crate::{
Templates,
edit_agent::{EditAgent, EditAgentOutputEvent},
edit_file_tool::EditFileToolCard,
schema::json_schema_for,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolResult};
use futures::StreamExt;
use gpui::{AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task};
use indoc::formatdoc;
use language_model::{
LanguageModelRegistry, LanguageModelRequestMessage, LanguageModelToolSchemaFormat,
};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
use ui::prelude::*;
use util::ResultExt;
pub struct StreamingEditFileTool;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct StreamingEditFileToolInput {
/// A one-line, user-friendly markdown description of the edit. This will be
/// shown in the UI and also passed to another model to perform the edit.
///
/// Be terse, but also descriptive in what you want to achieve with this
/// edit. Avoid generic instructions.
///
/// NEVER mention the file path in this description.
///
/// <example>Fix API endpoint URLs</example>
/// <example>Update copyright year in `page_footer`</example>
///
/// Make sure to include this field before all the others in the input object
/// so that we can display it immediately.
pub display_description: String,
/// The full path of the file to create or modify in the project.
///
/// WARNING: When specifying which file path need changing, you MUST
/// start each path with one of the project's root directories.
///
/// The following examples assume we have two root directories in the project:
/// - backend
/// - frontend
///
/// <example>
/// `backend/src/main.rs`
///
/// Notice how the file path starts with root-1. Without that, the path
/// would be ambiguous and the call would fail!
/// </example>
///
/// <example>
/// `frontend/db.js`
/// </example>
pub path: PathBuf,
/// If true, this tool will recreate the file from scratch.
/// If false, this tool will produce granular edits to an existing file.
///
/// When a file already exists or you just created it, always prefer editing
/// it as opposed to recreating it from scratch.
pub create_or_overwrite: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct PartialInput {
#[serde(default)]
path: String,
#[serde(default)]
display_description: String,
}
const DEFAULT_UI_TEXT: &str = "Editing file";
impl Tool for StreamingEditFileTool {
fn name(&self) -> String {
"edit_file".into()
}
fn needs_confirmation(&self, _: &serde_json::Value, _: &App) -> bool {
false
}
fn description(&self) -> String {
include_str!("streaming_edit_file_tool/description.md").to_string()
}
fn icon(&self) -> IconName {
IconName::Pencil
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
json_schema_for::<StreamingEditFileToolInput>(format)
}
fn ui_text(&self, input: &serde_json::Value) -> String {
match serde_json::from_value::<StreamingEditFileToolInput>(input.clone()) {
Ok(input) => input.display_description,
Err(_) => "Editing file".to_string(),
}
}
fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String {
if let Some(input) = serde_json::from_value::<PartialInput>(input.clone()).ok() {
let description = input.display_description.trim();
if !description.is_empty() {
return description.to_string();
}
let path = input.path.trim();
if !path.is_empty() {
return path.to_string();
}
}
DEFAULT_UI_TEXT.to_string()
}
fn run(
self: Arc<Self>,
input: serde_json::Value,
messages: &[LanguageModelRequestMessage],
project: Entity<Project>,
action_log: Entity<ActionLog>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
let input = match serde_json::from_value::<StreamingEditFileToolInput>(input) {
Ok(input) => input,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!(
"Path {} not found in project",
input.path.display()
)))
.into();
};
let Some(worktree) = project
.read(cx)
.worktree_for_id(project_path.worktree_id, cx)
else {
return Task::ready(Err(anyhow!("Worktree not found for project path"))).into();
};
let exists = worktree.update(cx, |worktree, cx| {
worktree.file_exists(&project_path.path, cx)
});
let card = window.and_then(|window| {
window
.update(cx, |_, window, cx| {
cx.new(|cx| {
EditFileToolCard::new(input.path.clone(), project.clone(), window, cx)
})
})
.ok()
});
let card_clone = card.clone();
let messages = messages.to_vec();
let task = cx.spawn(async move |cx: &mut AsyncApp| {
if !input.create_or_overwrite && !exists.await? {
return Err(anyhow!("{} not found", input.path.display()));
}
let model = cx
.update(|cx| LanguageModelRegistry::read_global(cx).default_model())?
.context("default model not set")?
.model;
let edit_agent = EditAgent::new(model, project.clone(), action_log, Templates::new());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await?;
let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let old_text = cx
.background_spawn({
let old_snapshot = old_snapshot.clone();
async move { old_snapshot.text() }
})
.await;
let (output, mut events) = if input.create_or_overwrite {
edit_agent.overwrite(
buffer.clone(),
input.display_description.clone(),
messages,
cx,
)
} else {
edit_agent.edit(
buffer.clone(),
input.display_description.clone(),
messages,
cx,
)
};
let mut hallucinated_old_text = false;
while let Some(event) = events.next().await {
match event {
EditAgentOutputEvent::Edited => {
if let Some(card) = card_clone.as_ref() {
let new_snapshot =
buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx
.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
})
.await;
card.update(cx, |card, cx| {
card.set_diff(
project_path.path.clone(),
old_text.clone(),
new_text,
cx,
);
})
.log_err();
}
}
EditAgentOutputEvent::OldTextNotFound(_) => hallucinated_old_text = true,
}
}
output.await?;
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
let new_text = cx.background_spawn({
let new_snapshot = new_snapshot.clone();
async move { new_snapshot.text() }
});
let diff = cx.background_spawn(async move {
language::unified_diff(&old_snapshot.text(), &new_snapshot.text())
});
let (new_text, diff) = futures::join!(new_text, diff);
if let Some(card) = card_clone {
card.update(cx, |card, cx| {
card.set_diff(project_path.path.clone(), old_text, new_text, cx);
})
.log_err();
}
let input_path = input.path.display();
if diff.is_empty() {
if hallucinated_old_text {
Err(anyhow!(formatdoc! {"
Some edits were produced but none of them could be applied.
Read the relevant sections of {input_path} again so that
I can perform the requested edits.
"}))
} else {
Ok("No edits were made.".to_string())
}
} else {
Ok(format!("Edited {}:\n\n```diff\n{}\n```", input_path, diff))
}
});
ToolResult {
output: task,
card: card.map(AnyToolCard::from),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn still_streaming_ui_text_with_path() {
let input = json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
"src/main.rs"
);
}
#[test]
fn still_streaming_ui_text_with_description() {
let input = json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_with_path_and_description() {
let input = json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_no_path_or_description() {
let input = json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
});
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
#[test]
fn still_streaming_ui_text_with_null() {
let input = serde_json::Value::Null;
assert_eq!(
StreamingEditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
}

View File

@@ -1,8 +0,0 @@
This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead.
Before using this tool:
1. Use the `read_file` tool to understand the file's contents and context
2. Verify the directory path is correct (only applicable when creating new files):
- Use the `list_directory` tool to verify the parent directory exists and is the correct location

View File

@@ -1,6 +1,4 @@
You are an expert text editor and your task is to produce a series of edits to a file given a description of the changes you need to make.
You MUST respond with a series of edits to that one file in the following format:
You MUST respond with a series of edits to a file, using the following format:
```
<edits>
@@ -51,3 +49,5 @@ Rules for editing:
<edit_description>
{{edit_description}}
</edit_description>
Tool calls have been disabled. You MUST start your response with <edits>.

View File

@@ -4,7 +4,7 @@ use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use futures::{FutureExt as _, future::Shared};
use gpui::{AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, WeakEntity, Window};
use language::LineEnding;
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
use project::{Project, terminals::TerminalKind};
use schemars::JsonSchema;
@@ -107,9 +107,10 @@ impl Tool for TerminalTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -178,7 +179,7 @@ impl Tool for TerminalTool {
let exit_status = child.wait()?;
let (processed_content, _) =
process_content(content, &input.command, Some(exit_status));
Ok(processed_content)
Ok(processed_content.into())
});
return ToolResult {
output: task,
@@ -266,7 +267,7 @@ impl Tool for TerminalTool {
card.elapsed_time = Some(card.start_instant.elapsed());
});
Ok(processed_content)
Ok(processed_content.into())
}
});
@@ -598,6 +599,7 @@ mod tests {
use editor::EditorSettings;
use fs::RealFs;
use gpui::{BackgroundExecutor, TestAppContext};
use language_model::fake_provider::FakeLanguageModel;
use pretty_assertions::assert_eq;
use serde_json::json;
use settings::{Settings, SettingsStore};
@@ -639,6 +641,7 @@ mod tests {
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
let model = Arc::new(FakeLanguageModel::default());
let input = TerminalToolInput {
command: "cat".to_owned(),
@@ -653,15 +656,16 @@ mod tests {
TerminalTool::run(
Arc::new(TerminalTool::new(cx)),
serde_json::to_value(input).unwrap(),
&[],
Arc::default(),
project.clone(),
action_log.clone(),
model,
None,
cx,
)
});
let output = result.output.await.log_err();
let output = result.output.await.log_err().map(|output| output.content);
assert_eq!(output, Some("Command executed successfully.".into()));
}
@@ -681,19 +685,25 @@ mod tests {
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone())));
let model = Arc::new(FakeLanguageModel::default());
let check = |input, expected, cx: &mut App| {
let headless_result = TerminalTool::run(
Arc::new(TerminalTool::new(cx)),
serde_json::to_value(input).unwrap(),
&[],
Arc::default(),
project.clone(),
action_log.clone(),
model.clone(),
None,
cx,
);
cx.spawn(async move |_| {
let output = headless_result.output.await.log_err();
let output = headless_result
.output
.await
.log_err()
.map(|output| output.content);
assert_eq!(output, expected);
})
};

View File

@@ -4,7 +4,7 @@ use crate::schema::json_schema_for;
use anyhow::{Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use gpui::{AnyWindowHandle, App, Entity, Task};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -47,15 +47,16 @@ impl Tool for ThinkingTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
_cx: &mut App,
) -> ToolResult {
// This tool just "thinks out loud" and doesn't perform any actions.
Task::ready(match serde_json::from_value::<ThinkingToolInput>(input) {
Ok(_input) => Ok("Finished thinking.".to_string()),
Ok(_input) => Ok("Finished thinking.".to_string().into()),
Err(err) => Err(anyhow!(err)),
})
.into()

View File

@@ -8,7 +8,7 @@ use futures::{Future, FutureExt, TryFutureExt};
use gpui::{
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -53,9 +53,10 @@ impl Tool for WebSearchTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_messages: &[LanguageModelRequestMessage],
_request: Arc<LanguageModelRequest>,
_project: Entity<Project>,
_action_log: Entity<ActionLog>,
_model: Arc<dyn LanguageModel>,
_window: Option<AnyWindowHandle>,
cx: &mut App,
) -> ToolResult {
@@ -72,7 +73,9 @@ impl Tool for WebSearchTool {
let search_task = search_task.clone();
async move {
let response = search_task.await.map_err(|err| anyhow!(err))?;
serde_json::to_string(&response).context("Failed to serialize search results")
serde_json::to_string(&response)
.context("Failed to serialize search results")
.map(Into::into)
}
});

View File

@@ -335,9 +335,13 @@ impl AutoUpdater {
self.status.clone()
}
pub fn dismiss_error(&mut self, cx: &mut Context<Self>) {
pub fn dismiss_error(&mut self, cx: &mut Context<Self>) -> bool {
if self.status == AutoUpdateStatus::Idle {
return false;
}
self.status = AutoUpdateStatus::Idle;
cx.notify();
true
}
// If you are packaging Zed and need to override the place it downloads SSH remotes from,

View File

@@ -155,6 +155,7 @@ pub fn notify_if_app_was_updated(cx: &mut App) {
}
cx.emit(DismissEvent);
})
.show_suppress_button(false)
})
},
);

View File

@@ -7,9 +7,10 @@ use anyhow::{Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
AutoToolChoice as BedrockAutoToolChoice, ContentBlock as BedrockInnerContent,
Tool as BedrockTool, ToolChoice as BedrockToolChoice, ToolConfiguration as BedrockToolConfig,
ToolInputSchema as BedrockToolInputSchema, ToolSpecification as BedrockToolSpec,
AnyToolChoice as BedrockAnyToolChoice, AutoToolChoice as BedrockAutoToolChoice,
ContentBlock as BedrockInnerContent, Tool as BedrockTool, ToolChoice as BedrockToolChoice,
ToolConfiguration as BedrockToolConfig, ToolInputSchema as BedrockToolInputSchema,
ToolSpecification as BedrockToolSpec,
};
pub use aws_smithy_types::Blob as BedrockBlob;
use aws_smithy_types::{Document, Number as AwsNumber};

View File

@@ -47,6 +47,7 @@ use std::{
};
use telemetry::Telemetry;
use thiserror::Error;
use tokio::net::TcpStream;
use url::Url;
use util::{ResultExt, TryFutureExt};
@@ -1127,7 +1128,10 @@ impl Client {
let stream = {
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
let _guard = handle.enter();
connect_socks_proxy_stream(proxy.as_ref(), rpc_host).await?
match proxy {
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
None => Box::new(TcpStream::connect(rpc_host).await?),
}
};
log::info!("connected to rpc endpoint {}", rpc_url);

View File

@@ -1,61 +1,98 @@
//! socks proxy
use anyhow::{Result, anyhow};
use anyhow::{Context, Result, anyhow};
use http_client::Url;
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
pub(crate) async fn connect_socks_proxy_stream(
proxy: Option<&Url>,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let stream = match parse_socks_proxy(proxy) {
Some((socks_proxy, SocksVersion::V4)) => {
let stream = Socks4Stream::connect_with_socket(
tokio::net::TcpStream::connect(socks_proxy).await?,
rpc_host,
)
.await
.map_err(|err| anyhow!("error connecting to socks {}", err))?;
Box::new(stream) as Box<dyn AsyncReadWrite>
}
Some((socks_proxy, SocksVersion::V5)) => Box::new(
Socks5Stream::connect_with_socket(
tokio::net::TcpStream::connect(socks_proxy).await?,
rpc_host,
)
.await
.map_err(|err| anyhow!("error connecting to socks {}", err))?,
) as Box<dyn AsyncReadWrite>,
None => {
Box::new(tokio::net::TcpStream::connect(rpc_host).await?) as Box<dyn AsyncReadWrite>
}
};
Ok(stream)
/// Identification to a Socks V4 Proxy
struct Socks4Identification<'a> {
user_id: &'a str,
}
fn parse_socks_proxy(proxy: Option<&Url>) -> Option<((String, u16), SocksVersion)> {
let proxy_url = proxy?;
let scheme = proxy_url.scheme();
/// Authorization to a Socks V5 Proxy
struct Socks5Authorization<'a> {
username: &'a str,
password: &'a str,
}
/// Socks Proxy Protocol Version
///
/// V4 allows idenfication using a user_id
/// V5 allows authorization using a username and password
enum SocksVersion<'a> {
V4(Option<Socks4Identification<'a>>),
V5(Option<Socks5Authorization<'a>>),
}
pub(crate) async fn connect_socks_proxy_stream(
proxy: &Url,
rpc_host: (&str, u16),
) -> Result<Box<dyn AsyncReadWrite>> {
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
// SOCKS proxies are often used in contexts where security and privacy are critical,
// so any fallback could expose users to significant risks.
return Err(anyhow!("Parsing proxy url failed"));
};
// Connect to proxy and wrap protocol later
let stream = tokio::net::TcpStream::connect(socks_proxy)
.await
.context("Failed to connect to socks proxy")?;
let socks: Box<dyn AsyncReadWrite> = match version {
SocksVersion::V4(None) => {
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V5(None) => {
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
let socks = Socks5Stream::connect_with_password_and_socket(
stream, rpc_host, username, password,
)
.await
.context("error connecting to socks")?;
Box::new(socks)
}
};
Ok(socks)
}
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
let scheme = proxy.scheme();
let socks_version = if scheme.starts_with("socks4") {
// socks4
SocksVersion::V4
let identification = match proxy.username() {
"" => None,
username => Some(Socks4Identification { user_id: username }),
};
SocksVersion::V4(identification)
} else if scheme.starts_with("socks") {
// socks, socks5
SocksVersion::V5
let authorization = proxy.password().map(|password| Socks5Authorization {
username: proxy.username(),
password,
});
SocksVersion::V5(authorization)
} else {
return None;
};
if let Some((host, port)) = proxy_url.host().zip(proxy_url.port_or_known_default()) {
Some(((host.to_string(), port), socks_version))
} else {
None
}
}
// private helper structs and traits
let host = proxy.host()?.to_string();
let port = proxy.port_or_known_default()?;
enum SocksVersion {
V4,
V5,
Some(((host, port), socks_version))
}
pub(crate) trait AsyncReadWrite:
@@ -66,3 +103,74 @@ impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> A
for T
{
}
#[cfg(test)]
mod tests {
use url::Url;
use super::*;
#[test]
fn parse_socks4() {
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(version, SocksVersion::V4(None)))
}
#[test]
fn parse_socks4_with_identification() {
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(
version,
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
))
}
#[test]
fn parse_socks5() {
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(version, SocksVersion::V5(None)))
}
#[test]
fn parse_socks5_with_authorization() {
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 1080);
assert!(matches!(
version,
SocksVersion::V5(Some(Socks5Authorization {
username: "username",
password: "password"
}))
))
}
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
/// SOCKS proxies are often used in contexts where security and privacy are critical,
/// so any fallback could expose users to significant risks.
#[tokio::test]
async fn fails_on_bad_proxy() {
// Should fail connecting because http is not a valid Socks proxy scheme
let proxy = Url::parse("http://localhost:2313").unwrap();
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
match result {
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
Ok(_) => panic!("Connecting on bad proxy should fail"),
};
}
}

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