Compare commits

...

65 Commits

Author SHA1 Message Date
gcp-cherry-pick-bot[bot]
71e4885874 Fix agent panel scroll while editing a past message (cherry-pick #30394) (#30399)
Cherry-picked Fix agent panel scroll while editing a past message
(#30394)

This is similar to the `block_mouse_down` method added in #20649 (which
has a very similar motivation), but is more comprehensive in stopping
mouse events. Since I want to cherry-pick this to the releases, keeping
this change just to the agent panel. In a follow-up will replace
existing use of `block_mouse_down` to instead use this.

Release Notes:

- N/A

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-05-09 16:31:16 -04:00
Marshall Bowers
ca9a1e58af assistant_context_editor: Register ContextEditor actions that were lost (#30428)
This PR restores the `ContextEditor` actions that were lost in
https://github.com/zed-industries/zed/pull/30168.

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

Release Notes:

- agent: Added back some missing actions, including `assistant: quote
selection`.
2025-05-09 13:17:34 -07:00
Max Brunsfeld
4fc7849bc3 Wait to locate system-installed Node until the shell environment is loaded (#30416)
Release Notes:

- Fixed a race condition that sometimes prevented a system-installed
`node` binary from being detected.
- Fixed a bug where the `node.path` setting was not respected when
invoking npm.
2025-05-09 13:17:26 -07:00
Danilo Leal
2ae7e167c2 agent: Fix layout shift due to the "Generating" label (#30422)
Closes https://github.com/zed-industries/zed/issues/30238

Release Notes:

- agent: Fixed layout shift happening in the toolbar (both in the
singleton and multibuffers) due to the "Generating" label that appeared
while the agent is still generating a response.
2025-05-09 15:23:11 -04:00
Agus Zubiaga
b061f44fc0 agent: Thread history update improvements (#30415)
- Try to preserve previously selected item on update
- Do not clear list items while updating to avoid a frame with no items
rendered

Release Notes:

- agent: Preserve previously selected item in Thread History on update

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
2025-05-09 15:23:11 -04:00
Joseph T. Lyons
9a2fb77732 zed 0.186.6 2025-05-09 14:28:48 -04:00
Cole Miller
5ba2869283 Revert "Load Profile state from Thread and tie visibility to the thread's model" (#30413)
This reverts commit 3615d6d96c.

Ultimately, we want to restore the ability to store a profile
per-thread, but for now reverting this fixes a fairly disruptive bug.

Release Notes:

- Fixed a bug causing the agent to use the wrong profile in some cases.
2025-05-09 14:10:29 -04:00
Ben Brandt
2c80aa103c agent: write latest profile change to default setting (#30408)
Reverts back to previous behavior where we update your settings so we
can load a new thread from your last configuration.

Release Notes:

- agent: Persist profile changes for new threads
2025-05-09 13:22:55 -04:00
Danilo Leal
2423b6d8c2 agent: Show delete thread icon buttons on hover/focus (#30370)
This PR's main goal is to show the delete thread button when the list
item is either focused or hovered. In order to do that, we ended up
refactoring (i.e., merging) the `PastThread` and `PastContext` elements
into a single `HistoryElementEntry` that already matches to the entry
type (i.e., context or thread).

Release Notes:

- agent: Simplify the UI by showing the delete thread icon button only
on hover or focus.

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
2025-05-09 12:57:34 -04:00
Michael Sloan
183912fdb2 Fix agent panel scroll while editing a past message (#30394)
This is similar to the `block_mouse_down` method added in #20649 (which
has a very similar motivation), but is more comprehensive in stopping
mouse events. Since I want to cherry-pick this to the releases, keeping
this change just to the agent panel. In a follow-up will replace
existing use of `block_mouse_down` to instead use this.

Release Notes:

- N/A
2025-05-09 12:57:28 -04:00
Michael Sloan
a21283b858 Misc improvement of code for agent markdown codeblock (#30388)
Release Notes:

- N/A
2025-05-09 12:56:18 -04:00
gcp-cherry-pick-bot[bot]
07868df84f askpass: Remove attempt to surface friendly error if zed exe path is not executable before use in askpass script (cherry-pick #30396) (#30397)
Cherry-picked askpass: Remove attempt to surface friendly error if zed
exe path is not executable before use in askpass script (#30396)

Closes #29819

Release Notes:

- Removed a faulty check in the askpass implementation causing
unintended "Failed to check metadata of Zed executable path for use in
askpass" errors when remoting via SSH or doing git operations that
require authentication.

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-05-09 11:55:24 -04:00
gcp-cherry-pick-bot[bot]
4a0025d7b5 Fix text overlap with markdown code block in list in agent response (cherry-pick #30377) (#30390)
Cherry-picked Fix text overlap with markdown code block in list in agent
response (#30377)

Seems that `h_full` was causing it to use the height of the overall list
item for some reason.

Closes #30002 

Release Notes:

- Agent Panel: Fixed text overlap for code blocks nested in lists in
agent response.

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-05-09 09:06:56 -06:00
Cole Miller
02f09a5d53 agent: Keyboard navigation improvements (#30274)
- Fix `ctrl-p` not working in the model selector
- Select first entry when opening the context picker

Release Notes:

- Fixed `menu::SelectPrevious` keybindings not working in the agent
panel's model selector.
2025-05-09 10:02:39 -04:00
Joseph T. Lyons
8afba12eb4 zed 0.186.5 2025-05-09 09:27:17 -04:00
Finn Evers
2c5d8f9889 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-09 09:19:03 -04:00
Oleksiy Syvokon
49689ef7c2 agent: Fuzzy search in model selector (#30281)
This change enables fuzzy search on model providers and names. For
example, the query "z41" will match "zed/gpt-4.1".

Release Notes:

- Agent: Improved model selection with fuzzy search support
2025-05-09 08:37:44 -04:00
Agus Zubiaga
5e9d6f473b agent: Render edit tool error as markdown (#30325)
Release Notes:

- agent: Render edit tool error as markdown and allow selecting it
2025-05-09 08:35:56 -04:00
Agus Zubiaga
b94b5e6982 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-09 08:35:15 -04:00
Agus Zubiaga
65ada5cd8f 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-09 08:35:09 -04:00
Shardul Vaidya
ac6f1c9003 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-09 08:34:02 -04:00
Danilo Leal
af88bd4e88 Add a divider below "Usage" in the Edit Prediction menu (#30284)
As it felt untidy without it.

Release Notes:

- N/A
2025-05-09 08:30:49 -04:00
gcp-cherry-pick-bot[bot]
3a73f7c4c8 Fix error when Copilot calls tools without arguments (cherry-pick #30371) (#30374)
Cherry-picked Fix error when Copilot calls tools without arguments
(#30371)

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

The model can output an empty string to indicate the absence of
arguments, which can't be parsed as a `serde_json::Value`. When that
happens, we now create an empty object instead on behalf of the model.

Release Notes:

- Fixed a bug that prevented Copilot models from calling the
`diagnostic` tool.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-09 14:17:39 +02:00
gcp-cherry-pick-bot[bot]
f3d84f0bc9 Include EditAgent's raw output when inspecting thread (cherry-pick #30337) (#30341)
Cherry-picked 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.

Co-authored-by: Antonio Scandurra <me@as-cii.com>
2025-05-09 12:02:17 +02:00
gcp-cherry-pick-bot[bot]
a230730d3a Make copilot::SignIn open sign-in modal when needed (cherry-pick #30239) (#30350)
Cherry-picked 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

Co-authored-by: Michael Sloan <michael@zed.dev>
2025-05-09 02:52:13 -06:00
gcp-cherry-pick-bot[bot]
5ae8072d22 agent: Show checkmark for current profile, not default profile (cherry-pick #30314) (#30334)
Cherry-picked 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.

Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-09 08:17:37 +03:00
gcp-cherry-pick-bot[bot]
f9b46ebf8a Restyle notification close control (cherry-pick #30262) (#30266)
Cherry-picked 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

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-08 17:34:40 +03:00
Joseph T. Lyons
762fc428a5 zed 0.186.4 2025-05-08 09:28:53 -04:00
Marshall Bowers
0490d255c6 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:15:48 -04:00
Ben Brandt
1d9c2ddbc7 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 09:13:13 -04:00
Antonio Scandurra
8594cefc97 Reuse conversation cache when streaming edits (#30245)
Release Notes:

- Improved latency when the agent applies edits.
2025-05-08 09:05:30 -04:00
gcp-cherry-pick-bot[bot]
1afd18629b debugger/extensions: Revert changes to extension store related to language config (cherry-pick #30225) (#30244)
Cherry-picked debugger/extensions: Revert changes to extension store
related to language config (#30225)

Revert #29945 

Release Notes:

- Fixed extension suggestions popping up over and over for recommended
extensions like Ruby or Nix.

---------

Co-authored-by: Conrad <conrad@zed.dev>

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Conrad <conrad@zed.dev>
2025-05-08 14:42:45 +02:00
Marshall Bowers
4aa38ce39a 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 08:34:29 -04:00
Marshall Bowers
8a742fba43 Remove assistant crate (#30168)
This PR removes the `assistant` crate, as it is no longer used.

Release Notes:

- N/A
2025-05-08 08:31:59 -04:00
Bennet Bo Fenner
055e35e2b9 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 08:28:04 -04:00
Ben Brandt
2c131c3d34 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 08:05:52 -04:00
Bennet Bo Fenner
206d7341a1 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 08:05:16 -04:00
versecafe
efbe678dcd mistral: Add new Mistral medium model (#30171)
Release Notes:

- Added `mistral-medium` to the Mistral provider.
2025-05-08 08:04:58 -04:00
Ben Brandt
0f9607710b 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-08 08:03:26 -04:00
gcp-cherry-pick-bot[bot]
91b1f60e5b Fix workspace update notifications not being suppressed (cherry-pick #30180) (#30205)
Cherry-picked Fix workspace update notifications not being suppressed
(#30180)

Follow-up of https://github.com/zed-industries/zed/pull/30015

Release Notes:

- N/A

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-08 09:20:02 +03:00
gcp-cherry-pick-bot[bot]
29bb9aaf76 Better match path-like strings in terminal (cherry-pick #30087) (#30207)
Cherry-picked 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

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-08 09:19:48 +03:00
gcp-cherry-pick-bot[bot]
9cd2806ce1 Do not flicker when switching cmd-hovered words in terminal (cherry-pick #30098) (#30206)
Cherry-picked 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

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-08 09:17:44 +03:00
gcp-cherry-pick-bot[bot]
96b328d0ff Add a way to clear activity indicator (cherry-pick #30156) (#30204)
Cherry-picked 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

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-05-08 09:16:16 +03:00
Joseph T. Lyons
da333757f1 zed 0.186.3 2025-05-07 21:07:58 -04:00
Max Brunsfeld
fb1ac6ef61 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 17:54:25 -07:00
Michael Sloan
190d6129bb 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-07 17:15:14 -04:00
Max Brunsfeld
a206c31e97 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 16:31:30 -04:00
Peter Tripp
e0b27290f3 zed 0.186.2 2025-05-07 14:09:43 -04:00
Danilo Leal
78f9852eda 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 14:05:59 -04:00
Agus Zubiaga
6eee3440e7 agent: Improve consecutive tool use callout spacing (#30145)
Release Notes:

- agent: Fix "consecutive tool use limit" callout spacing
2025-05-07 14:05:56 -04:00
Finn Evers
080ef043c3 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 14:05:53 -04:00
Marshall Bowers
a7b882c1cb 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 14:05:47 -04:00
Peter Tripp
202a19e0ed Legal Terms: May 6th 2025 update (#30151)
Updated terms for Agent panel launch.

Release Notes:

- N/A
2025-05-07 14:05:44 -04:00
Marshall Bowers
ace7f573c3 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 14:05:29 -04:00
Agus Zubiaga
d0da6f75d7 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 14:05:26 -04:00
Antonio Scandurra
13743ef2ef 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 14:05:22 -04:00
Mikayla Maki
03f9b1e74e Restore tool cards on thread deserialization (#30053)
Release Notes:

- N/A

---------

Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
2025-05-07 14:05:19 -04:00
Danilo Leal
453125eb03 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 11:55:47 -04:00
Richard Feldman
e8dadb16c3 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 11:41:52 -04:00
Antonio Scandurra
a1c5a58521 zed 0.186.1 2025-05-07 13:27:45 +02:00
gcp-cherry-pick-bot[bot]
703af39558 Fix zero-sized message editors when context strip is empty (cherry-pick #30079) (#30086)
Cherry-picked 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>

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Nathan Sobo <nathan@zed.dev>
2025-05-07 13:12:48 +02:00
gcp-cherry-pick-bot[bot]
dc9b1d316d Avoid panic when opening thread as markdown in non-local project (cherry-pick #30061) (#30063)
Cherry-picked 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.

Co-authored-by: Cole Miller <cole@zed.dev>
2025-05-07 09:02:52 +03:00
Marshall Bowers
affec54b8e Update onboading modal copy 2025-05-06 21:55:20 -04:00
Marshall Bowers
4cde597d39 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:44:39 -04:00
Joseph T. Lyons
2e2ad6c80e v0.186.x preview 2025-05-06 19:49:40 -04:00
172 changed files with 4004 additions and 32680 deletions

81
Cargo.lock generated
View File

@@ -56,13 +56,13 @@ dependencies = [
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
"assistant_slash_commands",
"assistant_tool",
"async-watch",
"buffer_diff",
"chrono",
"client",
"collections",
"command_palette_hooks",
"component",
"context_server",
"convert_case 0.8.0",
@@ -79,6 +79,7 @@ dependencies = [
"heed",
"html_to_markdown",
"http_client",
"indexed_docs",
"indoc",
"itertools 0.14.0",
"jsonschema",
@@ -471,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"
@@ -547,7 +486,6 @@ dependencies = [
"collections",
"context_server",
"editor",
"feature_flags",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -597,7 +535,6 @@ dependencies = [
"anyhow",
"collections",
"deepseek",
"feature_flags",
"fs",
"gpui",
"indexmap",
@@ -740,6 +677,7 @@ dependencies = [
"language_models",
"linkme",
"log",
"markdown",
"open",
"paths",
"portable-pty",
@@ -3005,7 +2943,6 @@ name = "collab"
version = "0.44.0"
dependencies = [
"anyhow",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_slash_command",
@@ -7864,9 +7801,12 @@ version = "0.1.0"
dependencies = [
"collections",
"feature_flags",
"futures 0.3.31",
"fuzzy",
"gpui",
"language_model",
"log",
"ordered-float 2.10.1",
"picker",
"proto",
"ui",
@@ -7907,6 +7847,7 @@ dependencies = [
"partial-json-fixer",
"project",
"proto",
"release_channel",
"schemars",
"serde",
"serde_json",
@@ -15088,7 +15029,6 @@ dependencies = [
"client",
"collections",
"db",
"feature_flags",
"gpui",
"http_client",
"notifications",
@@ -18697,7 +18637,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.186.0"
version = "0.186.6"
dependencies = [
"activity_indicator",
"agent",
@@ -18705,7 +18645,6 @@ dependencies = [
"ashpd",
"askpass",
"assets",
"assistant",
"assistant_context_editor",
"assistant_settings",
"assistant_tools",
@@ -18887,9 +18826,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" }
@@ -610,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]

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

@@ -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-loader-circle-icon lucide-loader-circle"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>

After

Width:  |  Height:  |  Size: 289 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

@@ -213,21 +213,6 @@
"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": {
@@ -713,8 +698,8 @@
{
"context": "PromptEditor",
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist",
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",
"ctrl-alt-e": "agent::RemoveAllContext"
}
},

View File

@@ -258,21 +258,6 @@
"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,
@@ -780,8 +765,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"
}
},
{

View File

@@ -1243,21 +1243,22 @@
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"Shell Script": [".env.*"]
},
// By default use a recent system version of node, or install our own.
// You can override this to use a version of node that is not in $PATH with:
// {
// "node": {
// "path": "/path/to/node"
// "npm_path": "/path/to/npm" (defaults to node_path/../npm)
// }
// }
// or to ensure Zed always downloads and installs an isolated version of node:
// {
// "node": {
// "ignore_system_version": true,
// }
// NOTE: changing this setting currently requires restarting Zed.
"node": {},
// Settings for which version of Node.js and NPM to use when installing
// language servers and Copilot.
//
// Note: changing this setting currently requires restarting Zed.
"node": {
// By default, Zed will look for `node` and `npm` on your `$PATH`, and use the
// existing executables if their version is recent enough. Set this to `true`
// to prevent this, and force Zed to always download and install its own
// version of Node.
"ignore_system_version": false,
// You can also specify alternative paths to Node and NPM. If you specify
// `path`, but not `npm_path`, Zed will assume that `npm` is located at
// `${path}/../npm`.
"path": null,
"npm_path": null
},
// The extensions that Zed should automatically install on startup.
//
// If you don't want any of these extensions, add this field to your settings

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,6 +86,25 @@ impl ActivityIndicator {
})
.detach();
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(
&project.read(cx).lsp_store(),
|_, _, event, cx| match event {
@@ -115,7 +136,7 @@ impl ActivityIndicator {
}
Self {
statuses: Default::default(),
statuses: Vec::new(),
project: project.clone(),
auto_updater,
context_menu_handle: Default::default(),
@@ -185,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,13 +23,13 @@ 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
chrono.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
context_server.workspace = true
convert_case.workspace = true
@@ -46,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;
@@ -327,6 +327,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
}
}
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK: usize = 10;
fn render_markdown_code_block(
@@ -485,12 +486,18 @@ fn render_markdown_code_block(
.copied_code_block_ids
.contains(&(message_id, ix));
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(true);
let can_expand = metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
let is_expanded = if can_expand {
active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, ix))
.copied()
.unwrap_or(false)
} else {
false
};
let codeblock_header_bg = cx
.theme()
@@ -511,7 +518,7 @@ fn render_markdown_code_block(
.children(label)
.child(
h_flex()
.visible_on_hover("codeblock_container")
.visible_on_hover(CODEBLOCK_CONTAINER_GROUP)
.gap_1()
.child(
IconButton::new(
@@ -553,45 +560,42 @@ fn render_markdown_code_block(
}
}),
)
.when(
metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
|header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
IconName::ChevronDown
},
)
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
.when(can_expand, |header| {
header.child(
IconButton::new(
("expand-collapse-code", ix),
if is_expanded {
IconName::ChevronUp
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
let is_expanded = this
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
cx.notify();
});
}
}),
IconName::ChevronDown
},
)
},
),
.icon_color(Color::Muted)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text(if is_expanded {
"Collapse Code"
} else {
"Expand Code"
}))
.on_click({
let active_thread = active_thread.clone();
move |_event, _window, cx| {
active_thread.update(cx, |this, cx| {
let is_expanded = this
.expanded_code_blocks
.entry((message_id, ix))
.or_insert(true);
*is_expanded = !*is_expanded;
cx.notify();
});
}
}),
)
}),
);
v_flex()
.group("codeblock_container")
.group(CODEBLOCK_CONTAINER_GROUP)
.my_2()
.overflow_hidden()
.rounded_lg()
@@ -599,16 +603,7 @@ fn render_markdown_code_block(
.border_color(cx.theme().colors().border.opacity(0.6))
.bg(cx.theme().colors().editor_background)
.child(codeblock_header)
.when(
metadata.line_count > MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK,
|this| {
if is_expanded {
this.h_full()
} else {
this.max_h_80()
}
},
)
.when(can_expand && !is_expanded, |this| this.max_h_80())
}
fn render_code_language(
@@ -712,7 +707,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)
@@ -721,7 +716,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)
@@ -1211,8 +1206,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);
});
}
})
@@ -1415,6 +1409,7 @@ impl ActiveThread {
mode: None,
messages: vec![request_message],
tools: vec![],
tool_choice: None,
stop: vec![],
temperature: AssistantSettings::temperature_for_model(
&configured_model.model,
@@ -1783,8 +1778,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"))
@@ -1809,13 +1803,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 => {
@@ -1828,11 +1825,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,
@@ -1850,7 +1844,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,
@@ -1871,7 +1864,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.",
)
@@ -1881,13 +1874,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(
@@ -1902,7 +1892,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(
@@ -2249,7 +2238,7 @@ impl ActiveThread {
// Backdrop to dim out the whole thread below the editing user message
parent.relative().child(
div()
.occlude()
.stop_mouse_events_except_scroll()
.absolute()
.inset_0()
.size_full()
@@ -2368,19 +2357,22 @@ impl ActiveThread {
let editor_bg = cx.theme().colors().editor_background;
move |el, range, metadata, _, cx| {
let can_expand = metadata.line_count
> MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK;
if !can_expand {
return el;
}
let is_expanded = active_thread
.read(cx)
.expanded_code_blocks
.get(&(message_id, range.start))
.copied()
.unwrap_or(true);
if is_expanded
|| metadata.line_count
<= MAX_UNCOLLAPSED_LINES_IN_CODE_BLOCK
{
.unwrap_or(false);
if is_expanded {
return el;
}
el.child(
div()
.absolute()
@@ -3261,15 +3253,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,
);
}
});
}
}
@@ -3448,6 +3443,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)
});
@@ -3525,7 +3525,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);
});
@@ -3534,7 +3534,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)
});

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

@@ -1,6 +1,4 @@
use crate::{
Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent, ui::AnimatedLabel,
};
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll, Thread, ThreadEvent};
use anyhow::Result;
use assistant_settings::AssistantSettings;
use buffer_diff::DiffHunkStatus;
@@ -11,8 +9,9 @@ use editor::{
scroll::Autoscroll,
};
use gpui::{
Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle,
Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity,
EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation,
WeakEntity, Window, percentage, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
@@ -25,6 +24,7 @@ use std::{
collections::hash_map::Entry,
ops::Range,
sync::Arc,
time::Duration,
};
use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider};
use util::ResultExt;
@@ -978,9 +978,20 @@ impl ToolbarItemView for AgentDiffToolbar {
impl Render for AgentDiffToolbar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let generating_label = div()
.w(rems_from_px(110.)) // Arbitrary size so the label doesn't dance around
.child(AnimatedLabel::new("Generating"))
let spinner_icon = div()
.px_0p5()
.id("generating")
.tooltip(Tooltip::text("Generating Changes…"))
.child(
Icon::new(IconName::LoadCircle)
.size(IconSize::Small)
.color(Color::Accent)
.with_animation(
"load_circle",
Animation::new(Duration::from_secs(3)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
),
)
.into_any();
let Some(active_item) = self.active_item.as_ref() else {
@@ -997,7 +1008,7 @@ impl Render for AgentDiffToolbar {
let content = match state {
EditorState::Idle => return Empty.into_any(),
EditorState::Generating => vec![generating_label],
EditorState::Generating => vec![spinner_icon],
EditorState::Reviewing => vec![
h_flex()
.child(
@@ -1115,7 +1126,7 @@ impl Render for AgentDiffToolbar {
let is_generating = agent_diff.read(cx).thread.read(cx).is_generating();
if is_generating {
return div().px_2().child(generating_label).into_any();
return div().px_2().child(spinner_icon).into_any();
}
let is_empty = agent_diff.read(cx).multibuffer.read(cx).is_empty();

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,
};
@@ -53,12 +53,12 @@ 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::history_store::{HistoryStore, RecentEntry};
use crate::message_editor::{MessageEditor, MessageEditorEvent};
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
use crate::thread_history::{HistoryEntryElement, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::ui::AgentOnboardingModal;
use crate::{
@@ -71,7 +71,7 @@ use crate::{
const AGENT_PANEL_KEY: &str = "agent_panel";
#[derive(Serialize, Deserialize)]
struct SerializedAssistantPanel {
struct SerializedAgentPanel {
width: Option<Pixels>,
}
@@ -80,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);
}
@@ -122,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);
@@ -132,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);
});
@@ -335,7 +335,7 @@ impl ActiveView {
}
}
pub struct AssistantPanel {
pub struct AgentPanel {
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
project: Entity<Project>,
@@ -349,13 +349,14 @@ 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,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
hovered_recent_history_item: Option<usize>,
assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
assistant_navigation_menu: Option<Entity<ContextMenu>>,
@@ -366,14 +367,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(())
@@ -423,7 +424,7 @@ impl AssistantPanel {
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
} else {
None
};
@@ -491,7 +492,6 @@ impl AssistantPanel {
thread_store.downgrade(),
context_store.downgrade(),
thread.clone(),
agent_panel_dock_position(cx),
window,
cx,
)
@@ -510,6 +510,7 @@ impl AssistantPanel {
thread_store.clone(),
context_store.clone(),
[RecentEntry::Thread(thread_id, thread.clone())],
window,
cx,
)
});
@@ -696,6 +697,7 @@ impl AssistantPanel {
previous_view: None,
history_store: history_store.clone(),
history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
hovered_recent_history_item: None,
assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
assistant_navigation_menu: None,
@@ -764,9 +766,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();
@@ -821,7 +823,6 @@ impl AssistantPanel {
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread,
agent_panel_dock_position(cx),
window,
cx,
)
@@ -967,7 +968,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| {
@@ -1030,7 +1031,6 @@ impl AssistantPanel {
self.thread_store.downgrade(),
self.context_store.downgrade(),
thread,
agent_panel_dock_position(cx),
window,
cx,
)
@@ -1165,15 +1165,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);
@@ -1203,9 +1201,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>,
@@ -1318,7 +1316,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),
@@ -1343,9 +1341,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"
}
@@ -1359,10 +1357,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,
@@ -1424,7 +1418,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…";
@@ -1983,9 +1977,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 =
@@ -2220,7 +2214,7 @@ impl AssistantPanel {
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Past Interactions")
Label::new("Recent")
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -2245,18 +2239,20 @@ impl AssistantPanel {
v_flex()
.gap_1()
.children(
recent_history.into_iter().map(|entry| {
recent_history.into_iter().enumerate().map(|(index, entry)| {
// TODO: Add keyboard navigation.
match entry {
HistoryEntry::Thread(thread) => {
PastThread::new(thread, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
.into_any_element()
}
HistoryEntry::Context(context) => {
PastContext::new(context, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
.into_any_element()
}
}
let is_hovered = self.hovered_recent_history_item == Some(index);
HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
.hovered(is_hovered)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_recent_history_item = Some(index);
} else if this.hovered_recent_history_item == Some(index) {
this.hovered_recent_history_item = None;
}
cx.notify();
}))
.into_any_element()
}),
)
)
@@ -2344,14 +2340,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> {
@@ -2751,44 +2746,18 @@ impl AssistantPanel {
}
}
impl Render for AssistantPanel {
impl Render for AgentPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let content = match &self.active_view {
ActiveView::Thread { .. } => 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))
.into_any(),
ActiveView::History => self.history.clone().into_any_element(),
ActiveView::PromptEditor {
context_editor,
buffer_search_bar,
..
} => self
.render_prompt_editor(context_editor, buffer_search_bar, window, cx)
.into_any(),
ActiveView::Configuration => v_flex()
.size_full()
.children(self.configuration.clone())
.into_any(),
};
let content = 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,
};
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()
@@ -2813,7 +2782,37 @@ impl Render for AssistantPanel {
.on_action(cx.listener(Self::reset_font_size))
.child(self.render_toolbar(window, cx))
.children(self.render_trial_upsell(window, cx))
.child(content)
.map(|parent| match &self.active_view {
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,
..
} => 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(),
}
}
}
@@ -2862,28 +2861,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()
}
@@ -2894,7 +2891,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")));
};
@@ -2921,12 +2918,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,160 +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 command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
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]);
const NAMESPACE: &str = "agent";
/// 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();
feature_gate_agent_actions(cx);
}
fn feature_gate_agent_actions(cx: &mut App) {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
}
})
.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

@@ -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;
@@ -381,6 +381,16 @@ impl ContextPicker {
cx.focus_self(window);
}
pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
match &self.mode {
ContextPickerState::Default(entity) => entity.update(cx, |entity, cx| {
entity.select_first(&Default::default(), window, cx)
}),
// Other variants already select their first entry on open automatically
_ => {}
}
}
fn recent_menu_item(
&self,
context_picker: Entity<ContextPicker>,
@@ -425,9 +435,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 +469,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 +487,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 +658,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();
@@ -420,12 +420,25 @@ impl Render for ContextStrip {
})
.child(
PopoverMenu::new("context-picker")
.menu(move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
.menu({
let context_picker = context_picker.clone();
move |window, cx| {
context_picker.update(cx, |this, cx| {
this.init(window, cx);
});
Some(context_picker.clone())
Some(context_picker.clone())
}
})
.on_open({
let context_picker = context_picker.downgrade();
Rc::new(move |window, cx| {
context_picker
.update(cx, |context_picker, cx| {
context_picker.select_first(window, cx);
})
.ok();
})
})
.trigger_with_tooltip(
IconButton::new("add-context", IconName::Plus)

View File

@@ -1,4 +1,4 @@
use std::{collections::VecDeque, path::Path};
use std::{collections::VecDeque, path::Path, sync::Arc};
use anyhow::{Context as _, anyhow};
use assistant_context_editor::{AssistantContext, SavedContextMetadata};
@@ -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::{
@@ -34,6 +34,20 @@ impl HistoryEntry {
HistoryEntry::Context(context) => context.mtime.to_utc(),
}
}
pub fn id(&self) -> HistoryEntryId {
match self {
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
}
}
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq)]
pub enum HistoryEntryId {
Thread(ThreadId),
Context(Arc<Path>),
}
#[derive(Clone, Debug)]
@@ -82,6 +96,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 +104,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

@@ -17,7 +17,6 @@ use editor::{
ToDisplayPoint,
},
};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
@@ -43,7 +42,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};
@@ -66,15 +65,6 @@ pub fn init(
InlineAssistant::update_global(cx, |inline_assistant, cx| {
inline_assistant.register_workspace(&workspace, window, cx)
});
cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
|is_assistant2_enabled, _workspace, _window, cx| {
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
});
}
})
.detach();
})
.detach();
}
@@ -97,7 +87,6 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
is_assistant2_enabled: bool,
}
impl Global for InlineAssistant {}
@@ -119,7 +108,6 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
is_assistant2_enabled: false,
}
}
@@ -188,18 +176,17 @@ impl InlineAssistant {
window: &mut Window,
cx: &mut App,
) {
let is_assistant2_enabled = self.is_assistant2_enabled;
let is_assistant2_enabled = true;
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 {
@@ -238,7 +225,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 {
@@ -251,15 +238,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 {
@@ -1462,7 +1449,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> {
@@ -1482,7 +1469,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();

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};
@@ -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>,
@@ -284,12 +284,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 {
@@ -921,7 +921,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),
@@ -1094,7 +1094,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::{
@@ -17,7 +17,6 @@ use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent,
EditorMode, EditorStyle, MultiBuffer,
};
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
use file_icons::FileIcons;
use fs::Fs;
use futures::future::Shared;
@@ -39,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};
@@ -67,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>,
@@ -134,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>,
@@ -152,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 {
@@ -200,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),
@@ -210,6 +199,16 @@ impl MessageEditor {
)
});
let profile_selector = cx.new(|cx| {
ProfileSelector::new(
fs,
thread.clone(),
thread_store,
editor.focus_handle(cx),
cx,
)
});
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
@@ -226,15 +225,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,
@@ -464,10 +455,6 @@ impl MessageEditor {
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
@@ -642,7 +629,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({
@@ -1074,10 +1061,6 @@ impl MessageEditor {
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let is_using_zed_provider = self
.thread
.read(cx)
@@ -1139,10 +1122,6 @@ impl MessageEditor {
token_usage_ratio: TokenUsageRatio,
cx: &mut Context<Self>,
) -> Option<Div> {
if !cx.has_flag::<NewBillingFeatureFlag>() {
return None;
}
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
"Thread reached the token limit"
} else {
@@ -1272,6 +1251,7 @@ impl MessageEditor {
mode: None,
messages: vec![request_message],
tools: vec![],
tool_choice: None,
stop: vec![],
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
};
@@ -1296,12 +1276,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(
@@ -1475,7 +1449,6 @@ impl AgentPreview for MessageEditor {
thread_store.downgrade(),
text_thread_store.downgrade(),
thread,
DockPosition::Left,
window,
cx,
)

View File

@@ -1,7 +1,8 @@
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::*};
@@ -13,24 +14,24 @@ use ui::{
};
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| {
@@ -40,19 +41,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 +65,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 +80,7 @@ impl ProfileSelector {
profile_id.clone(),
profile,
settings,
cx,
));
}
}
@@ -100,6 +101,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."),
@@ -112,7 +114,7 @@ impl ProfileSelector {
.toggleable(IconPosition::End, profile_id == settings.default_profile);
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 {
@@ -151,51 +153,67 @@ impl Render for ProfileSelector {
.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

@@ -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;
@@ -430,6 +431,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 +441,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);
@@ -1064,6 +1072,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(),
@@ -1144,6 +1153,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 +1198,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 +1235,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 +1312,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 +1587,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 +1929,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 +1947,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 +1956,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 +2051,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 +2066,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 +2080,10 @@ impl Thread {
} else {
tool.run(
input,
messages,
request,
self.project.clone(),
self.action_log.clone(),
model,
window,
cx,
)
@@ -2412,6 +2466,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

@@ -2,12 +2,11 @@ use std::fmt::Display;
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::{
App, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task,
UniformListScrollHandle, WeakEntity, Window, uniform_list,
};
use time::{OffsetDateTime, UtcOffset};
@@ -18,19 +17,21 @@ use ui::{
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,
hovered_index: Option<usize>,
search_editor: Entity<Editor>,
all_entries: Arc<Vec<HistoryEntry>>,
// 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>,
separated_items: Vec<ListItemType>,
// Maps entry indexes to list item indexes
separated_item_indexes: Vec<u32>,
_separated_items_task: Option<Task<()>>,
search_state: SearchState,
scrollbar_visibility: bool,
@@ -50,7 +51,7 @@ enum SearchState {
},
}
enum HistoryListItem {
enum ListItemType {
BucketSeparator(TimeBucket),
Entry {
index: usize,
@@ -58,18 +59,18 @@ enum HistoryListItem {
},
}
impl HistoryListItem {
impl ListItemType {
fn entry_index(&self) -> Option<usize> {
match self {
HistoryListItem::BucketSeparator(_) => None,
HistoryListItem::Entry { index, .. } => Some(*index),
ListItemType::BucketSeparator(_) => None,
ListItemType::Entry { index, .. } => Some(*index),
}
}
}
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 +97,15 @@ 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,
hovered_index: None,
search_state: SearchState::Empty,
all_entries: Default::default(),
separated_items: Default::default(),
separated_item_indexes: Default::default(),
search_editor,
scrollbar_visibility: true,
scrollbar_state,
@@ -114,54 +117,74 @@ impl ThreadHistory {
}
fn update_all_entries(&mut self, cx: &mut Context<Self>) {
self.all_entries = self
let new_entries: Arc<Vec<HistoryEntry>> = self
.history_store
.update(cx, |store, cx| store.entries(cx))
.into();
self.set_selected_index(0, cx);
self.update_separated_items(cx);
match &self.search_state {
SearchState::Empty => {}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
self.search(query.clone(), cx);
}
}
cx.notify();
}
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 = Vec::with_capacity(new_entries.len() + 1);
let mut indexes = Vec::with_capacity(new_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();
for (index, entry) in new_entries.iter().enumerate() {
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(ListItemType::BucketSeparator(entry_bucket));
}
separated_items.push(HistoryListItem::Entry {
indexes.push(items.len() as u32);
items.push(ListItemType::Entry {
index,
format: entry_bucket.into(),
});
}
separated_items
(new_entries, items, indexes)
});
let task = cx.spawn(async move |this, cx| {
let separated_items = bg_task.await;
let (new_entries, items, indexes) = bg_task.await;
this.update(cx, |this, cx| {
this.separated_items = separated_items;
let previously_selected_entry =
this.all_entries.get(this.selected_index).map(|e| e.id());
this.all_entries = new_entries;
this.separated_items = items;
this.separated_item_indexes = indexes;
match &this.search_state {
SearchState::Empty => {
if this.selected_index >= this.all_entries.len() {
this.set_selected_entry_index(
this.all_entries.len().saturating_sub(1),
cx,
);
} else if let Some(prev_id) = previously_selected_entry {
if let Some(new_ix) = this
.all_entries
.iter()
.position(|probe| probe.id() == prev_id)
{
this.set_selected_entry_index(new_ix, cx);
}
}
}
SearchState::Searching { query, .. } | SearchState::Searched { query, .. } => {
this.search(query.clone(), cx);
}
}
cx.notify();
})
.log_err();
@@ -229,7 +252,7 @@ impl ThreadHistory {
matches,
};
this.set_selected_index(0, cx);
this.set_selected_entry_index(0, cx);
cx.notify();
};
})
@@ -252,6 +275,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 +310,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 +326,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 +341,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 +410,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 +435,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)),
};
@@ -435,7 +475,7 @@ impl ThreadHistory {
.map(|(ix, m)| {
self.render_list_item(
Some(range_start + ix),
&HistoryListItem::Entry {
&ListItemType::Entry {
index: m.candidate_id,
format: EntryTimeFormat::DateAndTime,
},
@@ -453,25 +493,36 @@ impl ThreadHistory {
fn render_list_item(
&self,
list_entry_ix: Option<usize>,
item: &HistoryListItem,
item: &ListItemType,
highlight_positions: Vec<usize>,
cx: &App,
cx: &Context<Self>,
) -> AnyElement {
match item {
HistoryListItem::Entry { index, format } => match self.all_entries.get(*index) {
ListItemType::Entry { index, format } => match self.all_entries.get(*index) {
Some(entry) => h_flex()
.w_full()
.pb_1()
.child(self.render_history_entry(
entry,
list_entry_ix == Some(self.selected_index),
highlight_positions,
*format,
))
.child(
HistoryEntryElement::new(entry.clone(), self.agent_panel.clone())
.highlight_positions(highlight_positions)
.timestamp_format(*format)
.selected(list_entry_ix == Some(self.selected_index))
.hovered(list_entry_ix == self.hovered_index)
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = list_entry_ix;
} else if this.hovered_index == list_entry_ix {
this.hovered_index = None;
}
cx.notify();
}))
.into_any_element(),
)
.into_any(),
None => Empty.into_any_element(),
},
HistoryListItem::BucketSeparator(bucket) => div()
ListItemType::BucketSeparator(bucket) => div()
.px(DynamicSpacing::Base06.rems(cx))
.pt_2()
.pb_1()
@@ -483,33 +534,6 @@ impl ThreadHistory {
.into_any_element(),
}
}
fn render_history_entry(
&self,
entry: &HistoryEntry,
is_active: bool,
highlight_positions: Vec<usize>,
format: EntryTimeFormat,
) -> AnyElement {
match entry {
HistoryEntry::Thread(thread) => PastThread::new(
thread.clone(),
self.assistant_panel.clone(),
is_active,
highlight_positions,
format,
)
.into_any_element(),
HistoryEntry::Context(context) => PastContext::new(
context.clone(),
self.assistant_panel.clone(),
is_active,
highlight_positions,
format,
)
.into_any_element(),
}
}
}
impl Focusable for ThreadHistory {
@@ -575,7 +599,7 @@ impl Render for ThreadHistory {
uniform_list(
cx.entity().clone(),
"thread-history",
self.matched_count(),
self.list_item_count(),
Self::list_items,
)
.p_1()
@@ -591,155 +615,97 @@ impl Render for ThreadHistory {
}
#[derive(IntoElement)]
pub struct PastThread {
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
pub struct HistoryEntryElement {
entry: HistoryEntry,
agent_panel: WeakEntity<AgentPanel>,
selected: bool,
hovered: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
}
impl PastThread {
pub fn new(
thread: SerializedThreadMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
) -> Self {
impl HistoryEntryElement {
pub fn new(entry: HistoryEntry, agent_panel: WeakEntity<AgentPanel>) -> Self {
Self {
thread,
assistant_panel,
selected,
highlight_positions,
timestamp_format,
entry,
agent_panel,
selected: false,
hovered: false,
highlight_positions: vec![],
timestamp_format: EntryTimeFormat::DateAndTime,
on_hover: Box::new(|_, _, _| {}),
}
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn hovered(mut self, hovered: bool) -> Self {
self.hovered = hovered;
self
}
pub fn highlight_positions(mut self, positions: Vec<usize>) -> Self {
self.highlight_positions = positions;
self
}
pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_hover = Box::new(on_hover);
self
}
pub fn timestamp_format(mut self, format: EntryTimeFormat) -> Self {
self.timestamp_format = format;
self
}
}
impl RenderOnce for PastThread {
impl RenderOnce for HistoryEntryElement {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let summary = self.thread.summary;
let (id, summary, timestamp) = match &self.entry {
HistoryEntry::Thread(thread) => (
thread.id.to_string(),
thread.summary.clone(),
thread.updated_at.timestamp(),
),
HistoryEntry::Context(context) => (
context.path.to_string_lossy().to_string(),
context.title.clone().into(),
context.mtime.timestamp(),
),
};
let thread_timestamp = self.timestamp_format.format_timestamp(
&self.assistant_panel,
self.thread.updated_at.timestamp(),
cx,
);
let thread_timestamp =
self.timestamp_format
.format_timestamp(&self.agent_panel, timestamp, cx);
ListItem::new(SharedString::from(self.thread.id.to_string()))
ListItem::new(SharedString::from(id))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div().max_w_4_5().child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
),
)
.end_slot(
h_flex()
.gap_1p5()
.w_full()
.gap_2()
.justify_between()
.child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
)
.child(
Label::new(thread_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.tooltip(move |window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx).detach_and_log_err(cx);
})
.ok();
}
}),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = self.thread.id.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
})
.ok();
}
})
}
}
#[derive(IntoElement)]
pub struct PastContext {
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
}
impl PastContext {
pub fn new(
context: SavedContextMetadata,
assistant_panel: WeakEntity<AssistantPanel>,
selected: bool,
highlight_positions: Vec<usize>,
timestamp_format: EntryTimeFormat,
) -> Self {
Self {
context,
assistant_panel,
selected,
highlight_positions,
timestamp_format,
}
}
}
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.context.mtime.timestamp(),
cx,
);
ListItem::new(SharedString::from(
self.context.path.to_string_lossy().to_string(),
))
.rounded()
.toggle_state(self.selected)
.spacing(ListItemSpacing::Sparse)
.start_slot(
div().max_w_4_5().child(
HighlightedLabel::new(summary, self.highlight_positions)
.size(LabelSize::Small)
.truncate(),
),
)
.end_slot(
h_flex()
.gap_1p5()
.child(
Label::new(context_timestamp)
.color(Color::Muted)
.size(LabelSize::XSmall),
)
.child(
.on_hover(self.on_hover)
.end_slot::<IconButton>(if self.hovered || self.selected {
Some(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
@@ -748,31 +714,71 @@ impl RenderOnce for PastContext {
Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx)
})
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, _window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
})
.ok();
}
let agent_panel = self.agent_panel.clone();
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> =
match &self.entry {
HistoryEntry::Thread(thread) => {
let id = thread.id.clone();
Box::new(move |_event, _window, cx| {
agent_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
HistoryEntry::Context(context) => {
let path = context.path.clone();
Box::new(move |_event, _window, cx| {
agent_panel
.update(cx, |this, cx| {
this.delete_context(path.clone(), cx)
.detach_and_log_err(cx);
})
.ok();
})
}
};
f
}),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let path = self.context.path.clone();
move |_event, window, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
}
})
)
} else {
None
})
.on_click({
let agent_panel = self.agent_panel.clone();
let f: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> = match &self.entry
{
HistoryEntry::Thread(thread) => {
let id = thread.id.clone();
Box::new(move |_event, window, cx| {
agent_panel
.update(cx, |this, cx| {
this.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
HistoryEntry::Context(context) => {
let path = context.path.clone();
Box::new(move |_event, window, cx| {
agent_panel
.update(cx, |this, cx| {
this.open_saved_prompt_editor(path.clone(), window, cx)
.detach_and_log_err(cx);
})
.ok();
})
}
};
f
})
}
}
@@ -785,12 +791,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,
)
})
@@ -772,6 +776,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)]

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()
@@ -143,7 +160,7 @@ impl Render for AgentOnboardingModal {
.full_width()
.on_click(cx.listener(Self::view_blog));
let copy = "Zed natively supports agentic editing, enabling seamless collaboration between humans and AI.";
let copy = "Zed now natively supports agentic editing, enabling fluid collaboration between humans and AI.";
base.child(Label::new(copy).color(Color::Muted)).child(
v_flex()

View File

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

View File

@@ -167,16 +167,20 @@ fn get_shell_safe_zed_path() -> anyhow::Result<String> {
.to_string_lossy()
.to_string();
// sanity check on unix systems that the path exists and is executable
// todo(windows): implement this check for windows (or just use `is-executable` crate)
use std::os::unix::fs::MetadataExt;
let metadata = std::fs::metadata(&zed_path)
.context("Failed to check metadata of Zed executable path for use in askpass")?;
let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
anyhow::ensure!(
is_executable,
"Failed to verify Zed executable path for use in askpass"
);
// NOTE: this was previously enabled, however, it caused errors when it shouldn't have
// (see https://github.com/zed-industries/zed/issues/29819)
// The zed path failing to execute within the askpass script results in very vague ssh
// authentication failed errors, so this was done to try and surface a better error
//
// use std::os::unix::fs::MetadataExt;
// let metadata = std::fs::metadata(&zed_path)
// .context("Failed to check metadata of Zed executable path for use in askpass")?;
// let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
// anyhow::ensure!(
// is_executable,
// "Failed to verify Zed executable path for use in askpass"
// );
// As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
// but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
// errors are introduced in the future :(

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 +0,0 @@
../../LICENSE-GPL

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

@@ -22,7 +22,6 @@ clock.workspace = true
collections.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true

View File

@@ -9,7 +9,8 @@ mod slash_command_picker;
use std::sync::Arc;
use client::Client;
use gpui::App;
use gpui::{App, Context};
use workspace::Workspace;
pub use crate::context::*;
pub use crate::context_editor::*;
@@ -18,6 +19,18 @@ pub use crate::context_store::*;
pub use crate::patch::*;
pub use crate::slash_command::*;
pub fn init(client: Arc<Client>, _cx: &mut App) {
pub fn init(client: Arc<Client>, cx: &mut App) {
context_store::init(&client.into());
workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
cx.observe_new(
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
workspace
.register_action(ContextEditor::quote_selection)
.register_action(ContextEditor::insert_selection)
.register_action(ContextEditor::copy_code)
.register_action(ContextEditor::handle_insert_dragged_files);
},
)
.detach();
}

View File

@@ -2585,6 +2585,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)),

View File

@@ -18,7 +18,6 @@ use editor::{
scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt as _};
use fs::Fs;
use futures::FutureExt;
use gpui::{
@@ -138,7 +137,7 @@ pub enum ThoughtProcessStatus {
Completed,
}
pub trait AssistantPanelDelegate {
pub trait AgentPanelDelegate {
fn active_context_editor(
&self,
workspace: &mut Workspace,
@@ -172,7 +171,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>()
@@ -185,7 +184,7 @@ impl dyn AssistantPanelDelegate {
}
}
struct GlobalAssistantPanelDelegate(Arc<dyn AssistantPanelDelegate>);
struct GlobalAssistantPanelDelegate(Arc<dyn AgentPanelDelegate>);
impl Global for GlobalAssistantPanelDelegate {}
@@ -1666,11 +1665,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;
};
@@ -1696,9 +1695,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 {
@@ -1731,11 +1730,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;
};
@@ -1821,7 +1820,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;
};
@@ -1852,7 +1851,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(
@@ -2395,19 +2394,11 @@ impl ContextEditor {
.on_click({
let focus_handle = self.focus_handle(cx).clone();
move |_event, window, cx| {
if cx.has_flag::<Assistant2FeatureFlag>() {
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
} else {
focus_handle.dispatch_action(
&zed_actions::assistant::ShowConfiguration,
window,
cx,
);
};
focus_handle.dispatch_action(
&zed_actions::agent::OpenConfiguration,
window,
cx,
);
}
}),
)
@@ -3368,10 +3359,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

@@ -15,7 +15,6 @@ path = "src/assistant_settings.rs"
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
collections.workspace = true
feature_flags.workspace = true
gpui.workspace = true
indexmap.workspace = true
language_model.workspace = true

View File

@@ -7,7 +7,6 @@ use anthropic::Model as AnthropicModel;
use anyhow::{Result, bail};
use collections::IndexMap;
use deepseek::Model as DeepseekModel;
use feature_flags::{AgentStreamEditsFeatureFlag, Assistant2FeatureFlag, FeatureFlagAppExt};
use gpui::{App, Pixels, SharedString};
use language_model::{CloudModel, LanguageModel};
use lmstudio::Model as LmStudioModel;
@@ -107,16 +106,8 @@ impl AssistantSettings {
.and_then(|m| m.temperature)
}
pub fn stream_edits(&self, cx: &App) -> bool {
cx.has_flag::<AgentStreamEditsFeatureFlag>() || self.stream_edits
}
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
if cx.has_flag::<Assistant2FeatureFlag>() {
return false;
}
cx.is_staff() || self.enable_experimental_live_diffs
pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool {
false
}
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
@@ -370,7 +361,7 @@ impl AssistantSettingsContent {
&model,
None,
None,
language_model.supports_tools(),
Some(language_model.supports_tools()),
)),
api_url,
});

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,15 +21,12 @@ mod web_search_tool;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use assistant_tool::ToolRegistry;
use copy_path_tool::CopyPathTool;
use feature_flags::{AgentStreamEditsFeatureFlag, FeatureFlagAppExt};
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::*;
@@ -40,21 +34,19 @@ 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 terminal_tool::TerminalTool;
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
@@ -75,12 +67,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_flag::<AgentStreamEditsFeatureFlag, _>(|_, cx| register_edit_file_tool(cx))
.detach();
cx.observe_global::<SettingsStore>(register_edit_file_tool)
.detach();
registry.register_tool(EditFileTool);
register_web_search_tool(&LanguageModelRegistry::global(cx), cx);
cx.subscribe(
@@ -107,29 +94,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,
@@ -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

@@ -11,7 +11,7 @@ use postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak};
use text::ReplicaId;
use util::TryFutureExt as _;
use util::{TryFutureExt as _, maybe};
pub type UserId = u64;
@@ -101,6 +101,7 @@ pub struct UserStore {
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_plan: Option<proto::Plan>,
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
trial_started_at: Option<DateTime<Utc>>,
model_request_usage_amount: Option<u32>,
model_request_usage_limit: Option<proto::UsageLimit>,
@@ -166,6 +167,7 @@ impl UserStore {
by_github_login: Default::default(),
current_user: current_user_rx,
current_plan: None,
subscription_period: None,
trial_started_at: None,
model_request_usage_amount: None,
model_request_usage_limit: None,
@@ -333,6 +335,13 @@ impl UserStore {
) -> Result<()> {
this.update(&mut cx, |this, cx| {
this.current_plan = Some(message.payload.plan());
this.subscription_period = maybe!({
let period = message.payload.subscription_period?;
let started_at = DateTime::from_timestamp(period.started_at as i64, 0)?;
let ended_at = DateTime::from_timestamp(period.ended_at as i64, 0)?;
Some((started_at, ended_at))
});
this.trial_started_at = message
.payload
.trial_started_at
@@ -713,6 +722,10 @@ impl UserStore {
self.current_plan
}
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
self.subscription_period
}
pub fn trial_started_at(&self) -> Option<DateTime<Utc>> {
self.trial_started_at
}

View File

@@ -76,7 +76,6 @@ workspace-hack.workspace = true
zed_llm_client.workspace = true
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
assistant_context_editor.workspace = true
assistant_settings.workspace = true
assistant_slash_command.workspace = true

View File

@@ -2709,7 +2709,7 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
let billing_customer = db.get_billing_customer_by_user_id(user_id).await?;
let billing_preferences = db.get_billing_preferences(user_id).await?;
let usage = if let Some(llm_db) = session.app_state.llm_db.clone() {
let (subscription_period, usage) = if let Some(llm_db) = session.app_state.llm_db.clone() {
let subscription = db.get_active_billing_subscription(user_id).await?;
let subscription_period = crate::db::billing_subscription::Model::current_period(
@@ -2717,15 +2717,17 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
session.is_staff(),
);
if let Some((period_start_at, period_end_at)) = subscription_period {
let usage = if let Some((period_start_at, period_end_at)) = subscription_period {
llm_db
.get_subscription_usage_for_period(user_id, period_start_at, period_end_at)
.await?
} else {
None
}
};
(subscription_period, usage)
} else {
None
(None, None)
};
session
@@ -2743,6 +2745,12 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
billing_preferences
.map(|preferences| preferences.model_request_overages_enabled)
},
subscription_period: subscription_period.map(|(started_at, ended_at)| {
proto::SubscriptionPeriod {
started_at: started_at.timestamp() as u64,
ended_at: ended_at.timestamp() as u64,
}
}),
usage: usage.map(|usage| {
let plan = match plan {
proto::Plan::Free => zed_llm_client::Plan::Free,

View File

@@ -6,8 +6,8 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use futures::StreamExt;
use gpui::{
AnyElement, App, AsyncWindowContext, Context, CursorStyle, DismissEvent, Element, Entity,
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
AnyElement, App, AsyncWindowContext, ClickEvent, Context, CursorStyle, DismissEvent, Element,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
ListScrollEvent, ListState, ParentElement, Render, StatefulInteractiveElement, Styled, Task,
WeakEntity, Window, actions, div, img, list, px,
};
@@ -811,24 +811,50 @@ impl NotificationToast {
}
impl Render for NotificationToast {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let user = self.actor.clone();
let suppress = window.modifiers().shift;
let (close_id, close_icon) = if suppress {
("suppress", IconName::Minimize)
} else {
("close", IconName::Close)
};
h_flex()
.id("notification_panel_toast")
.elevation_3(cx)
.p_2()
.gap_2()
.justify_between()
.children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
.child(Label::new(self.text.clone()))
.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
.child(
IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(
IconButton::new("suppress", IconName::XCircle)
.tooltip(Tooltip::text("Do not show until restart"))
.on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
IconButton::new(close_id, close_icon)
.tooltip(move |window, cx| {
if suppress {
Tooltip::for_action(
"Suppress.\nClose with click.",
&workspace::SuppressNotification,
window,
cx,
)
} else {
Tooltip::for_action(
"Close.\nSuppress with shift-click",
&menu::Cancel,
window,
cx,
)
}
})
.on_click(cx.listener(move |_, _: &ClickEvent, _, cx| {
if suppress {
cx.emit(SuppressEvent);
} else {
cx.emit(DismissEvent);
}
})),
)
.on_click(cx.listener(|this, _, window, cx| {
this.focus_notification_panel(window, cx);

View File

@@ -3,6 +3,7 @@ mod copilot_completion_provider;
pub mod request;
mod sign_in;
use crate::sign_in::initiate_sign_in_within_workspace;
use ::fs::Fs;
use anyhow::{Result, anyhow};
use collections::{HashMap, HashSet};
@@ -24,6 +25,7 @@ use node_runtime::NodeRuntime;
use parking_lot::Mutex;
use request::StatusNotification;
use settings::SettingsStore;
use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
use std::{
any::TypeId,
env,
@@ -34,9 +36,10 @@ use std::{
sync::Arc,
};
use util::{ResultExt, fs::remove_matching};
use workspace::Workspace;
pub use crate::copilot_completion_provider::CopilotCompletionProvider;
pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in};
pub use crate::sign_in::{CopilotCodeVerification, initiate_sign_in, reinstall_and_sign_in};
actions!(
copilot,
@@ -99,27 +102,25 @@ pub fn init(
})
.detach();
cx.on_action(|_: &SignIn, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
}
});
cx.on_action(|_: &SignOut, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.sign_out(cx))
.detach_and_log_err(cx);
}
});
cx.on_action(|_: &Reinstall, cx| {
if let Some(copilot) = Copilot::global(cx) {
copilot
.update(cx, |copilot, cx| copilot.reinstall(cx))
.detach();
}
});
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|workspace, _: &SignIn, window, cx| {
if let Some(copilot) = Copilot::global(cx) {
let is_reinstall = false;
initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
}
});
workspace.register_action(|workspace, _: &Reinstall, window, cx| {
if let Some(copilot) = Copilot::global(cx) {
reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
}
});
workspace.register_action(|workspace, _: &SignOut, _window, cx| {
if let Some(copilot) = Copilot::global(cx) {
sign_out_within_workspace(workspace, copilot, cx);
}
});
})
.detach();
}
enum CopilotServer {
@@ -563,7 +564,7 @@ impl Copilot {
.ok();
}
pub fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
pub(crate) fn sign_in(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
if let CopilotServer::Running(server) = &mut self.server {
let task = match &server.sign_in_status {
SignInStatus::Authorized { .. } => Task::ready(Ok(())).shared(),
@@ -647,7 +648,7 @@ impl Copilot {
}
}
pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
pub(crate) fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
match &self.server {
CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
@@ -667,7 +668,7 @@ impl Copilot {
}
}
pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Task<()> {
pub(crate) fn reinstall(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
let language_settings = all_language_settings(None, cx);
let env = self.build_env(&language_settings.edit_predictions.copilot);
let start_task = cx
@@ -689,7 +690,7 @@ impl Copilot {
cx.notify();
cx.background_spawn(start_task)
start_task
}
pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {

View File

@@ -182,11 +182,11 @@ pub enum Tool {
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
#[serde(rename_all = "lowercase")]
pub enum ToolChoice {
Auto,
Any,
Tool { name: String },
None,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]

View File

@@ -12,7 +12,7 @@ use workspace::{ModalView, Toast, Workspace};
const COPILOT_SIGN_UP_URL: &str = "https://github.com/features/copilot";
struct CopilotStartingToast;
struct CopilotStatusToast;
pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
let Some(copilot) = Copilot::global(cx) else {
@@ -21,50 +21,83 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
let Some(workspace) = window.root::<Workspace>().flatten() else {
return;
};
workspace.update(cx, |workspace, cx| {
let is_reinstall = false;
initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx)
});
}
pub fn reinstall_and_sign_in(window: &mut Window, cx: &mut App) {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let Some(workspace) = window.root::<Workspace>().flatten() else {
return;
};
workspace.update(cx, |workspace, cx| {
reinstall_and_sign_in_within_workspace(workspace, copilot, window, cx);
});
}
pub fn reinstall_and_sign_in_within_workspace(
workspace: &mut Workspace,
copilot: Entity<Copilot>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let _ = copilot.update(cx, |copilot, cx| copilot.reinstall(cx));
let is_reinstall = true;
initiate_sign_in_within_workspace(workspace, copilot, is_reinstall, window, cx);
}
pub fn initiate_sign_in_within_workspace(
workspace: &mut Workspace,
copilot: Entity<Copilot>,
is_reinstall: bool,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
if matches!(copilot.read(cx).status(), Status::Disabled) {
copilot.update(cx, |this, cx| this.start_copilot(false, true, cx));
copilot.update(cx, |copilot, cx| copilot.start_copilot(false, true, cx));
}
match copilot.read(cx).status() {
Status::Starting { task } => {
workspace.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot is starting...",
),
cx,
);
});
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStatusToast>(),
if is_reinstall {
"Copilot is reinstalling..."
} else {
"Copilot is starting..."
},
),
cx,
);
let workspace = workspace.downgrade();
cx.spawn(async move |cx| {
cx.spawn_in(window, async move |workspace, cx| {
task.await;
if let Some(copilot) = cx.update(|cx| Copilot::global(cx)).ok().flatten() {
if let Some(copilot) = cx.update(|_window, cx| Copilot::global(cx)).ok().flatten() {
workspace
.update(cx, |workspace, cx| match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStartingToast>(),
"Copilot has started!",
),
cx,
),
_ => {
workspace.dismiss_toast(
&NotificationId::unique::<CopilotStartingToast>(),
.update_in(cx, |workspace, window, cx| {
match copilot.read(cx).status() {
Status::Authorized => workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStatusToast>(),
"Copilot has started.",
),
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
if let Some(window_handle) = cx.active_window() {
window_handle
.update(cx, |_, window, cx| {
workspace.toggle_modal(window, cx, |_, cx| {
CopilotCodeVerification::new(&copilot, cx)
});
})
.log_err();
),
_ => {
workspace.dismiss_toast(
&NotificationId::unique::<CopilotStatusToast>(),
cx,
);
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach_and_log_err(cx);
workspace.toggle_modal(window, cx, |_, cx| {
CopilotCodeVerification::new(&copilot, cx)
});
}
}
})
@@ -74,16 +107,54 @@ pub fn initiate_sign_in(window: &mut Window, cx: &mut App) {
.detach();
}
_ => {
copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
workspace.update(cx, |this, cx| {
this.toggle_modal(window, cx, |_, cx| {
CopilotCodeVerification::new(&copilot, cx)
});
copilot
.update(cx, |copilot, cx| copilot.sign_in(cx))
.detach();
workspace.toggle_modal(window, cx, |_, cx| {
CopilotCodeVerification::new(&copilot, cx)
});
}
}
}
pub fn sign_out_within_workspace(
workspace: &mut Workspace,
copilot: Entity<Copilot>,
cx: &mut Context<Workspace>,
) {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStatusToast>(),
"Signing out of Copilot...",
),
cx,
);
let sign_out_task = copilot.update(cx, |copilot, cx| copilot.sign_out(cx));
cx.spawn(async move |workspace, cx| match sign_out_task.await {
Ok(()) => {
workspace
.update(cx, |workspace, cx| {
workspace.show_toast(
Toast::new(
NotificationId::unique::<CopilotStatusToast>(),
"Signed out of Copilot.",
),
cx,
)
})
.ok();
}
Err(err) => {
workspace
.update(cx, |workspace, cx| {
workspace.show_error(&err, cx);
})
.ok();
}
})
.detach();
}
pub struct CopilotCodeVerification {
status: Status,
connect_clicked: bool,

View File

@@ -1,12 +1,11 @@
use std::{
borrow::Cow,
cmp::Reverse,
ops::Not,
path::{Path, PathBuf},
sync::Arc,
usize,
};
use collections::{HashMap, HashSet};
use dap::{
DapRegistry, DebugRequest,
adapters::{DebugAdapterName, DebugTaskDefinition},
@@ -192,25 +191,22 @@ impl NewSessionModal {
cx: &mut Context<Self>,
) -> Option<ui::DropdownMenu> {
let workspace = self.workspace.clone();
let language_registry = self
.workspace
.update(cx, |this, _| this.app_state().languages.clone())
.ok()?;
let weak = cx.weak_entity();
let label = self
.debugger
.as_ref()
.map(|d| d.0.clone())
.unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
let active_buffer_language_name =
self.task_contexts
.active_item_context
.as_ref()
.and_then(|item| {
item.1
.as_ref()
.and_then(|location| location.buffer.read(cx).language()?.name().into())
});
let active_buffer_language = self
.task_contexts
.active_item_context
.as_ref()
.and_then(|item| {
item.1
.as_ref()
.and_then(|location| location.buffer.read(cx).language())
})
.cloned();
DropdownMenu::new(
"dap-adapter-picker",
label,
@@ -229,42 +225,19 @@ impl NewSessionModal {
}
};
let available_languages = language_registry.language_names();
let mut debugger_to_languages = HashMap::default();
for language in available_languages {
let Some(language) =
language_registry.available_language_for_name(language.as_str())
else {
continue;
};
language.config().debuggers.iter().for_each(|adapter| {
debugger_to_languages
.entry(adapter.clone())
.or_insert_with(HashSet::default)
.insert(language.name());
});
}
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
.ok()
.unwrap_or_default();
available_adapters.sort_by_key(|name| {
let languages_for_debugger = debugger_to_languages.get(name.as_ref());
let languages_count =
languages_for_debugger.map_or(0, |languages| languages.len());
let contains_language_of_active_buffer = languages_for_debugger
.zip(active_buffer_language_name.as_ref())
.map_or(false, |(languages, active_buffer_language)| {
languages.contains(active_buffer_language)
});
(
Reverse(contains_language_of_active_buffer),
Reverse(languages_count),
)
});
if let Some(language) = active_buffer_language {
available_adapters.sort_by_key(|adapter| {
language
.config()
.debuggers
.get_index_of(adapter.0.as_ref())
.unwrap_or(usize::MAX)
});
}
for adapter in available_adapters.into_iter() {
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));

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