Compare commits

..

325 Commits

Author SHA1 Message Date
Cole Miller
7dc833d932 zed 0.204.3 2025-09-19 10:13:44 -04:00
Cole Miller
85cfd726c8 python: Install basedpyright with npm instead of pip (#38471)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-09-19 10:05:09 -04:00
Joseph T. Lyons
9ae63f04f0 zed 0.204.2 2025-09-18 16:00:22 -04:00
Cole Miller
9bfe7a1798 acp: Fix agent servers sometimes not being registered when Zed starts (#38330)
In local projects, initialize the list of agents in the agent server
store immediately. Previously we were initializing the list only after a
delay, in an attempt to avoid sending the `ExternalAgentsUpdated`
message to the downstream client (if any) before its handlers were
initialized. But we already have a separate codepath for that situation,
in the `AgentServerStore::shared`, and we can insert the delay in that
place instead.

Release Notes:

- acp: Fixed a bug where starting an external agent thread soon after
Zed starts up would show a "not registered" error.

---------

Co-authored-by: Michael <michael@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
2025-09-18 15:56:39 -04:00
Cole Miller
10f172ddac python: Install basedpyright if the basedpyright-langserver binary is missing (#38426)
Potential fix for #38377

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <petertripp@gmail.com>
2025-09-18 15:52:13 -04:00
Ben Brandt
a62514df7a acp: Fix behavior of read_text_file for ACP agents (#38401)
We were incorrectly handling the line number as well as stripping out
line breaks when returning portions of files.

It also makes sure following is updated even when we load a snapshot
from cache, which wasn't the case before.

We also are able to load the text via a range in the snapshot, rather
than allocating a string for the entire file and then another after
iterating over lines in the file.

Release Notes:

- acp: Fix incorrect behavior when ACP agents requested to read portions
of files.
2025-09-18 11:55:45 +02:00
Vladimir Varankin
97d04621de Hide BasedPyright banner in toolbar when dismissed (#38135)
This PR fixes the `BasedPyrightBanner`, making sure the banner is
completely hidden in the toolbar, when it was dismissed, or it's not
installed.

Without the fix, the banner still occupies some space in the toolbar,
making the UI looks inconsistent when editing a Python file. The bug is
**especially prominent** when the toolbar is hidden in the user's
settings (see below).

_Banner is shown_
<img width="1470" height="254" alt="Screenshot 2025-09-14 at 11 36 37"
src="https://github.com/user-attachments/assets/1415b075-0660-41ed-8069-c2318ac3a7cf"
/>

_Banner dismissed_
<img width="1470" height="207" alt="Screenshot 2025-09-14 at 11 36 44"
src="https://github.com/user-attachments/assets/828a3fba-5c50-4aba-832c-3e0cc6ed464b"
/>

_Banner dismissed (and the toolbar is hidden)_
<img width="1470" height="177" alt="Screenshot 2025-09-14 at 12 07 25"
src="https://github.com/user-attachments/assets/41aa5861-87df-491f-ac7e-09fc1558dd84"
/>

Closes n/a

Release Notes:

- Fixed the basedpyright onboarding banner
2025-09-18 00:40:13 +02:00
Joseph T. Lyons
4dc0720741 Adjust release notes Discord webhook name (#37997)
Release Notes:

- N/A
2025-09-17 11:23:33 -04:00
Joseph T. Lyons
8127523d0f v0.204.x stable 2025-09-17 10:10:04 -04:00
Smit Barmase
a1c8e69283 macOS: Disable NSAutoFillHeuristicController on macOS 26 (#38179)
Closes #33182

From
https://github.com/zed-industries/zed/issues/33182#issuecomment-3289846957,
thanks @mitchellh.

Release Notes:

- Fixed an issue where scrolling could sometimes feel choppy on macOS
26.
2025-09-16 21:53:09 +05:30
Ben Brandt
3a79ce7312 agent_servers: Set proxy env for all ACP agents (#38247)
- Use ProxySettings::proxy_url to read from settings or env
- Export HTTP(S)_PROXY and NO_PROXY for agent CLIs
- Add read_no_proxy_from_env and move parsing from main

Closes https://github.com/zed-industries/claude-code-acp/issues/46

Release Notes:

- acp: Pass proxy settings through to all ACP agents
2025-09-16 12:28:05 +02:00
Ben Brandt
06e58c318b agent_servers: Let Gemini CLI know it is running in Zed (#38058)
By passing through Zed as the surface, Gemini can know which editor it
is running in.

Release Notes:

- N/A
2025-09-16 12:26:56 +02:00
Conrad Irwin
64adb6d461 Re-add VSCode syntax node motions (#38208)
Closes #ISSUE

Release Notes:

- (preview only) restored ctrl-shift-{left,right} for Larger/Smaller
syntax node. This is VSCode's default and avoids the breaking change
from #37874
2025-09-15 09:18:34 -06:00
Ben Kunkle
69a7c3e252 Ability to update JSON arrays (#38087)
Closes #ISSUE

Adds the ability to our JSON updating code to update arrays within other
objects. Previously updating of arrays was limited to just top level
arrays (i.e. `keymap.json`) however this PR makes it so nested arrays
are supported as well using `#{index}` syntax as a key.

This PR also fixes an issue with the array updating code that meant that
updating empty json values `""` or an empty `keymap.json` file in the
case of the Keymap Editor would fail instead of creating a new array.

Release Notes:

- Fixed an issue where keybindings would fail to save in the Keymap
Editor if the `keymap.json` file was completely empty
2025-09-14 11:55:59 -05:00
Ben Kunkle
41737efb07 Fix line indicator format setting (#38071)
Closes #ISSUE

Release Notes:

- Fixed an issue where the `line_indicator_format` setting would not
update based on the value in `settings.json`
2025-09-12 16:17:22 -04:00
Anthony
608991db39 zed 0.204.1 2025-09-11 19:14:58 -04:00
Anthony Eid
8e0e2a887c Fix auto update not defaulting to true (#38022)
#37337 Made `AutoUpdateSetting` `FileContent =
AutoUpdateSettingsContent` which caused a deserialization bug to occur
because the field it was wrapping wasn't optional. Thus serde would
deserialize the wrapped type `bool` to its default value `false`
stopping the settings load function from reading the correct default
value from `default.json`

I also added a log message that states when the auto updater struct is
checking for updates to make this easier to test.

Release Notes:

- fix auto update defaulting to false
2025-09-11 19:11:33 -04:00
Michael Sloan
d900f12473 Fix panics from unicode slicing in license detection 2025-09-11 14:44:07 -06:00
Joseph T. Lyons
b3729e0b90 v0.204.x preview 2025-09-10 10:08:23 -04:00
Lukas Wirth
d110d325d4 languages: Remove broken pixi environment activation (#37915)
Closes [#ISSUE](https://github.com/zed-industries/zed/issues/37895)

This needs more thought behind it to be implemented properly

Release Notes:

- N/A
2025-09-10 13:35:16 +00:00
Bennet Bo Fenner
2cf3def716 acp: Add keybindings for authorizing tool calls (#37876)
TODO:
- [x] Double-check if we like the naming of the new actions
- [x] Only show keybinding hint once per option (e.g. if there are two
`allow_once` buttons only show it on the first one)
- [x] If there are multiple tool calls that need authorisation, only
show keybindings on the first tool call
- [x] Figure out which keybindings to use
- [x] Add linux keybindings
- [x] Add windows keybindings
- [x] Bug: long keybindings can make the buttons overflow


Release Notes:

- Add keybindings for authorizing tool calls (`agent: Allow once`,
`agent: Allow always`, `agent: Reject once`) in the agent panel

---------

Co-authored-by: Agus Zubiaga <agus@zed.dev>
2025-09-10 13:23:46 +00:00
Lukas Wirth
df2d097dc5 editor: Fix adjacent custom highlights interfering with each other (#37912)
Release Notes:

- Fixed matching bracket highlighting not highlighting closing brackets
when adjacent to each other

Co-authored-by: Finn Evers <finn@zed.dev>
2025-09-10 13:08:32 +00:00
Kirill Bulatov
fcdd427cf8 Revert "markdown: Add support for inline HTML img tags inside text (#37264)" (#37893)
This reverts commit e1a5d29972.

This have regressed Zed release notes' wrapping which we do not want to
do on a release day:
https://github.com/zed-industries/zed/pull/37264#issuecomment-3265420442

Release Notes:

- N/A
2025-09-10 05:34:42 +00:00
Smit Barmase
9c548a0ec6 workspace: Fix tab bar drop target height when no unpinned tabs present (#37884)
Before:
<img width="846" height="192" alt="Screenshot 2025-09-10 at 4 44 18 AM"
src="https://github.com/user-attachments/assets/3c79e140-e2b2-4e50-9fce-cb182e46d878"
/>

After:
<img width="846" height="192" alt="Screenshot 2025-09-10 at 4 43 13 AM"
src="https://github.com/user-attachments/assets/b5fa853d-ce39-4c81-9773-1d84eebc8cbb"
/>

Release Notes:

- Fixed height of the drop background in the tab bar when no unpinned
tabs are present.
2025-09-10 05:17:36 +05:30
Michael Sloan
bd0a5dd664 Potentially fix welcome banner in agent panel not going away (#37879)
Potentially fixes #37367. Just going ahead with the change even though
it's unclear whether this is the fix, since it is quite low risk.

Release Notes:

- N/A
2025-09-09 21:13:12 +00:00
Michael Sloan
2f40a3bdfa keymap validation: Improve message for action that takes no input (#37877)
See
https://github.com/zed-industries/zed/issues/26370#issuecomment-3200022302

Before (user screenshot):

<img width="650" height="127" alt="Image"
src="https://github.com/user-attachments/assets/9548ce43-657e-46ef-b6be-b95489167ac2"
/>

After (my screenshot):

<img width="616" height="229" alt="image"
src="https://github.com/user-attachments/assets/09028305-e7bb-4a31-bb87-55effbec26f3"
/>

Release Notes:

- N/A
2025-09-09 20:56:29 +00:00
Mitch (a.k.a Voz)
304af661a0 Fix import not working on first column first row of file (#37746)
Closes #26136

Release Notes:

- fixed auto import not working on first column, first row of file
2025-09-09 14:18:27 -06:00
Santiago Bernhardt
d2886d606b ollama: Add mistral-nemo (#37723)
Adding mistral nemo, sorting by name and adding comment about clamp
sizing

Release Notes:

- Added support for mistral-nemo

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-09 20:08:05 +00:00
Joseph T. Lyons
cffb883108 Consistently map syntax node navigation actions on macOS (#37874)
Skipping Linux and Windows keymaps here, as it's hard to find a
consistent base-binding for all 4 actions across all platforms that
don't break important actions for each keymap. Someone else can think on
that and make a proposal.

Release Notes:

- Added bindings for navigating between sibling nodes in the syntax tree
on macOS (`cmd-ctrl-{up/down}` for `SelectPreviousSyntaxNode` and
`SelectNextSyntaxNode`). Breaking change: the existing syntax tree
parent/child navigation bindings have moved from
`ctrl-shift-{left/right}` to `cmd-ctrl-{left/right}` to create a unified
four-directional navigation pattern where all syntax tree operations use
the same modifier combination. We could not use the previous base
modifiers without breaking more bindings.
2025-09-09 20:07:42 +00:00
Kirill Bulatov
eb7154d099 Skip "open this directory" menu entry in case of errors (#37872)
Follow-up of https://github.com/zed-industries/zed/pull/37564

Release Notes:

- N/A
2025-09-09 19:53:03 +00:00
0xshadow
18c6d9d394 Fix SVG preview not refreshing on external file changes (#37316)
Closes #37208 

## Release Notes:

- Fixed: SVG preview now refreshes automatically when files are modified
by external programs

## Summary

Previously, SVG preview would only refresh when files were saved within
the Zed editor, but not when modified by external programs (like
scripts, other editors, etc.)

## What Changed

The SVG preview now subscribes to file system events through the
worktree system. When an external program modifies an SVG file, the
worktree detects the change and notifies the preview. The preview then
clears its cache and refreshes to show the updated content.

## Before the fix



https://github.com/user-attachments/assets/e7f9a2b2-50f9-4b43-95e9-93a0720749f5


## After the fix


https://github.com/user-attachments/assets/b23511e3-8e59-45a1-b29b-d5105d32bd2c

AI Usage:
Used Cursor for code generation
2025-09-09 13:36:35 -06:00
Mitch (a.k.a Voz)
414d3be437 Set usePlaceholders to match Go default (#37551)
Closes #33629

Release Notes:

- changed gopls default to match what Go specifies
https://github.com/golang/tools/blob/master/gopls/doc/settings.md#useplaceholders-bool
2025-09-09 13:32:45 -06:00
Alvaro Parker
0862a0b666 git: Add uncommit action (#37031)
Closes #36767

Release Notes:

- Add uncommit action for git
2025-09-09 13:29:49 -06:00
Umesh Yadav
2e36e9782e language_models: Make Copilot Chat resilient to new model vendors and add tokenizer-based token counting (#37118)
While working on fixing this: #37116. I reliased the current
implementation of github copilot is not truly resilient to upstream
changes.

This PR enhances GitHub Copilot Chat to be forward-compatible with new
AI model vendors and improves token counting accuracy by using
vendor-specific tokenizers from the GitHub Copilot API. The system
previously failed when GitHub added new model vendors like xAI with
deserialization errors, and token counting wasn't utilizing the
vendor-specific tokenizer information provided by the API. The solution
adds an Unknown variant to the ModelVendor enum with serde other
attribute to gracefully handle any new vendors GitHub introduces,
implements tokenizer-aware token counting that uses the model's
specified tokenizer mapping o200k_base to gpt-4o with fallback, adds
explicit support for xAI models with proper tool input format handling,
and includes comprehensive test coverage for unknown vendor scenarios.
Key changes include adding the tokenizer field to model capabilities,
implementing the tokenizer method on models, updating tool input format
logic to handle unknown vendors, and simplifying token counting to use
the vendor's specified tokenizer or fall back to gpt-4o. This ensures
Zed's Copilot Chat integration remains robust and accurate as GitHub
continues expanding their AI model provider ecosystem.

Release Notes:

- Enhanced model vendor compatibility to automatically support future AI
providers and improved token counting accuracy using vendor-specific
tokenizers from the GitHub Copilot

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-09 13:28:26 -06:00
Mitch (a.k.a Voz)
1751bf4cdb Allow outline modal toggling (#37575)
Closes #37511 

The outline modal seems to have a bug where if it's open and the
`outline::Toggle` is triggered, it would not close if there was another
command with the same keybind. So instead, if the outline modal is open
and an `outline::Toggle` is triggered, we dismiss the modal.

Release Notes:

- Fixed a bug where `outline::Toggle` would sometimes not close outline
modal
2025-09-09 13:26:35 -06:00
Jonathan Hart
2fae4c7c72 vim: Make indenting selected lines with > and < in Helix mode no longer deselect them (#37665)
Improves Helix compatibility by making the Indent keybinds `<` and `>`
no longer deselect lines if they're selected.

Post Indent action current Zed release:
<img width="485" height="271" alt="image"
src="https://github.com/user-attachments/assets/41fd3d94-9c89-49dd-adc5-f696dbd79827"
/>
(Cursor is on the beginning of the first line)

Post Indent action in Helix:
<img width="376" height="144" alt="image"
src="https://github.com/user-attachments/assets/fdd1a489-cf3a-4638-9199-3adffa63ef61"
/>

Post Indent action in this PR:
<img width="463" height="300" alt="image"
src="https://github.com/user-attachments/assets/8edd3b81-e446-4b55-bc90-61cac3661ed7"
/>

Release Notes:

- Fixed selected lines indented with `<` and `>` deselecting in Helix
mode
2025-09-09 13:18:22 -06:00
chbk
1ae3d25aed Improve Markdown highlighting (#37669)
Release Notes:

  - Improved Markdown syntax highlighting
  
  
PR #25330 raised concerns about breaking changes, so I split it into two
PRs:
- This PR improves highlighting without deprecating existing selectors.
- PR #37670 is based on this PR and introduces new Markdown selectors.

| Zed 0.202.7 | With this PR |
| --- | --- |
| <img width="800" height="1080" alt="md-0 202 7"
src="https://github.com/user-attachments/assets/0b0b5920-32ed-454f-bc3b-bf5cd0a9086a"
/> | <img width="800" height="1080" alt="md-pr"
src="https://github.com/user-attachments/assets/a5923a2e-391e-4e09-a60b-1a142f9378f3"
/> |

Changes to include the `markup` selector, conforming to
[Neovim](38e46a6d7a/queries/markdown/highlights.scm (L59)),
[VS
Code](dfad570d15/extensions/markdown-basics/syntaxes/markdown.tmLanguage.json (L60)),
[Atom](6686ac6ccc/grammars/gfm.json (L147)),
and [Zed
itself](1e255e41cc/crates/languages/src/gitcommit/highlights.scm (L1)).

- `paragraph`, `indented_code_block`, `pipe_table`: `text`
- `# Heading`: `title` -> `title.markup`
- `-`, `1.`, `>`, `|`: `punctuation.markup`
- ```` ``` ````: `punctuation.embedded.markup`
- `[1]: url.com`, `[link](url.com)`: `link_text.markup`
- `url.com`: `link_uri` -> `link_uri.markup`
- `*italic*`: `emphasis` -> `emphasis.markup`
- `**bold**`: `emphasis.strong` -> `emphasis.strong.markup`
- ``` `raw` ```: `text.literal` -> `text.literal.markup`
- `~~strikethrough~~`: `strikethrough.markup`

````md

# Heading

Some stylized text:
- `raw`
- *italic*
- ~strike~
- **strong**

> quoted

```python
print("some code")
```

1. Here is an ![image](image.jpg)
2. A [link](https://github.com/zed-industries)
3. And even a [referenced link][1]

[1]: https://zed.dev

| tables | are |
| --- | --- |
| properly | scoped |

````
2025-09-09 13:05:07 -06:00
Ben Brandt
5e58f44d85 Fix auth in edit_agent evals (#37869)
Somehow we have a regression where the auth wasn't being called, so the
model didn't exist.

Looking at the code, it is likely this was relying on some other part of
the code doing the auth, since the order wouldn't have worked before
without that happening. This new order of doing auth before checking for
available models should fix it going forward.

Release Notes:

- N/A
2025-09-09 19:00:24 +00:00
Jakub Konka
d8085d3ac0 zed: Hide Install CLI menu button on Windows (#37843)
The "Install CLI" menu button and `install_cli::Install` action are
effectively no-op on Windows since the CLI is directly available in
Windows Terminal (CMD prompt, PowerShell, etc.) after the user runs Zed
installer package.

Release Notes:

- N/A

Co-authored-by: Peter Tripp <peter@zed.dev>
2025-09-09 20:56:08 +02:00
Anthony Eid
707d0e6ebd settings ui: Add text field support to ui layer (#37868)
This is an initial implementation that isn't used for any settings yet,
but will be used once `Vec<String>` is implemented.

I also updated the window.with_state api to grant access to a
`Context<S>` app reference instead of just an App.

## Example

<img width="603" height="83" alt="Screenshot 2025-09-09 at 2 15 56 PM"
src="https://github.com/user-attachments/assets/7b3fc350-a157-431f-a4bc-80a1806a3147"
/>


Release Notes:

- N/A
2025-09-09 18:48:22 +00:00
Lev Zakharov
46fb521333 git_ui: Show author name on commits in branch picker (#36812)
See related discussion
https://github.com/zed-industries/zed/discussions/36511.

<img alt="zed"
src="https://github.com/user-attachments/assets/da7fc30d-2504-48f4-a392-7c8d5cd8acb1"
/>

Release Notes:

- Added option to show the author name in a branch picker commit
information

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-09 18:44:46 +00:00
Ivan Trubach
9529cd18d1 project_panel: Add action to open file in split pane (#36973)
Closes #18605

Related:
- https://github.com/zed-industries/zed/discussions/16901
- https://github.com/zed-industries/zed/issues/10549

Adds an action to open file in split pane. Also updates vim keybindings
for better compatibility with netrw:
[“v”](bc461f952d/runtime/pack/dist/opt/netrw/doc/netrw.txt (L1091))
and
[“o”](bc461f952d/runtime/pack/dist/opt/netrw/doc/netrw.txt (L1075))
keys should open file in splits.

<table>
<tr>
<td><video
src="https://github.com/user-attachments/assets/e24c6fd4-4eb0-407a-bdd1-5300908ea2a1">
<td><video
src="https://github.com/user-attachments/assets/fa0b8105-41a9-4421-a3cc-89244a90d67a">
</table>


Release Notes:

- Added `project_panel::OpenSplitVertical` and
`project_panel::OpenSplitHorizontal` actions for opening file in a
splits.
2025-09-10 00:14:15 +05:30
Marco Munizaga
14ffd7b53f editor: Implement Go to next/prev Document Highlight (#35994)
Closes #21193
Closes #14703 

Having the ability to navigate directly to the next
symbolHighlight/reference lets you follow the data flow of a variable.
If you highlight the function itself (depending on the LSP), you can
also navigate to all returns.

Note that this is a different feature from navigating to the next match,
as that is not language-context aware. For example, if you have a var
named foo it would also navigate to an unrelated variable fooBar.

Here's how this patch works:

- The editor struct has a background_highlights.
- Collect all highlights with the keys [DocumentHighlightRead,
DocumentHighlightWrite]
- Depending on the direction, move the cursor to the next or previous
highlight relative to the current position.

Release Notes:

- Added `editor::GoToNextDocumentHighlight` and
`editor::GoToPreviousDocumentHighlight` to navigate to the next LSP
document highlight. Useful for navigating to the next usage of a certain
symbol.
2025-09-09 18:38:38 +00:00
Jacob
9431c65733 git: Improve error messages (#35946)
Release Notes:

- Improved git error messages

Includes stderr in the error message for git commands, provides better
output for things like errors when switching branches.

Before:
<img width="702" height="330" alt="image"
src="https://github.com/user-attachments/assets/f32402ae-b85c-4b0b-aae8-789607e8ec9e"
/>

After:
<img width="650" height="575" alt="image"
src="https://github.com/user-attachments/assets/308dbe3c-1ff9-40b9-a187-1e12d2488c80"
/>

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-09 18:16:29 +00:00
Paul Sadauskas
b2d7e34e80 Update Editor::select_larger_syntax_node (#36971)
When the cursor was sitting on a syntactically insignificant character,
like a `{` or `,`, this function was selecting only that character, when
what the user likely wanted was to select the next larger syntax node.

Those punctuation characters all seemed to be not "named", in
tree-sitter terminology, so I updated the function to walk up the node
tree until it found a node where `is_named()` is true.

Closes #4555 

Also, while writing the tests, the output of a failing test with the
wrong thing selected was harder to read than it needed to be.

It used to output a diff of ranges, like this:

<img width="217" height="111" alt="image"
src="https://github.com/user-attachments/assets/00de53a8-8776-47aa-8101-5a5b5bc3fa5e"
/>

I leveraged the existing `generate_marked_text` helper function and
updated the assertion to output a diff of the text with the selection
markers:

<img width="211" height="116" alt="image"
src="https://github.com/user-attachments/assets/53b2b882-2676-4c70-8718-e2e2ba6f254e"
/>

Happy to make that a separate PR, if needed.

Release Notes:

- Fixed Editor select_larger_syntax_node to be smart about punctuation.
2025-09-09 12:13:20 -06:00
Antonio Scandurra
61d4718f2b Make it possible to support GPUI in the scheduler crate (#37849)
Added features that weren't needed in our cloud code.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
2025-09-09 18:10:03 +00:00
nick-kilian
9e903c9fd1 Add path/status sorting toggle to git panel menu (#35704)
Adds a new menu option to toggle between sorting git entries by path or
status, with settings integration

Release Notes:

- Git Panel: Added toggle to switch between sorting git panel entries by
path or by status (available in git panel ellipsis menu)

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-09 17:55:25 +00:00
Warpten
d81479ee57 Allow user-defined worktree names in title bar and platform windows (#36713)
Closes #36637 

Release Notes:
- Adds the ability to specify a human-readable project name for each
worktree.


https://github.com/user-attachments/assets/ce980fa6-65cf-46d7-9343-d08c800914fd
2025-09-09 11:50:58 -06:00
Piotr Osiewicz
a2edd56587 python: Add import paths to Pyright/BasedPyright completion items (#37865)
Release Notes:

- python: Added package origin to completions from Pyright/BasedPyright
2025-09-09 17:50:18 +00:00
Agus Zubiaga
d7a9be03d1 acp: Ensure connection subprocess gets killed on drop (#37858)
It appears that in macOS, the `AcpConnection._wait_task` doesn't always
get dropped when quitting the app. In these cases, the subprocess would
be kept alive because we move the `child` into it.

Instead, we will now explicitly kill it when `AcpConnection` is dropped.
It's ok to do this because when the connection is dropped, the thread is
also dropped, so there's no need to report the exit status to it.

Closes #37741

Release Notes:

- Claude Code: Fix subprocess leak on app quit
2025-09-09 17:22:19 +00:00
Conrad Irwin
cdbddc2170 Allow unauthenticated commit models to show (#37857)
Closes #37462
Closes #37814

Release Notes:

- Fixed a bug where the commit generation message would not always show
2025-09-09 17:05:49 +00:00
localcc
f397294640 Fix git staging in a subfolder (#37860)
Closes #37418 

Ideal fix for this would be #37855 but that is a large refactor so
implementing this as a workaround.

Release Notes:

- N/A
2025-09-09 16:50:26 +00:00
Umesh Yadav
8527dcfc65 git_ui: Better handle commit message amend (#35268)
Follow up to this: #35114

* Previously we were still showing the commit message even after
cancelling amend which was the old commit message.
* This PR fixes that by restoring the commit message to the old state
before the amend begin so that in case user typed a commit message it's
shown if not then it's not.

Before:


https://github.com/user-attachments/assets/e0edcfff-863d-4367-a7c4-8a2998e702ca

After:



https://github.com/user-attachments/assets/9daf9be0-8a3d-4914-91a4-567693711b6b




Release Notes:

- Resolved an issue where cancelling an amend commit would incorrectly
leave the old commit message in the input field. The commit message box
now properly reverts to its pre-amend state.
2025-09-09 10:37:53 -06:00
Guillaume Launay
7f607a9b7d git_ui: Show current branch first in branch picker (#35138)
Closes #ISSUE

Release Notes:

- Put current branch first in branch picker
2025-09-09 10:36:26 -06:00
Agus Zubiaga
5e397e85b1 acp: Support session modes (e.g. CC plan mode) (#37632)
Adds support for [ACP session
modes](https://github.com/zed-industries/agent-client-protocol/pull/67)
enabling plan and other permission modes in CC:


https://github.com/user-attachments/assets/dea18d82-4da6-465e-983b-02b77c6dcf15


Release Notes:

- Claude Code: Add support for plan mode, and all other permission modes

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-09 13:28:02 -03:00
Marshall Bowers
ad02f6b9e3 cloud_llm_client: Add another Plan variant (#37852)
This PR adds a corresponding `FreeV2` variant to the `Plan`.

Release Notes:

- N/A
2025-09-09 15:51:22 +00:00
Piotr Osiewicz
2e7607c0e7 python: Fix instability of Pyright/BasedPyright code completions (#37850)
Pyright sets different `sortText` based on whether a given completion
item was recently resolved. This probably lines up with VSCode's way of
resolving items, but it's a no-no for us, as it makes completions
unstable.

Closes #9983

Release Notes:

- python: Fixed code completions having arbitrary order when using
Pyright/basedpyright
2025-09-09 15:34:57 +00:00
Joseph Mearman
0ac1752668 terminal: Sanitize trailing periods in URL detection (#37684)
Fixes #12338, related to #37616

This change improves URL detection in the terminal by removing trailing
periods that appear to be sentence punctuation rather than part of the
URL structure. It builds upon the parentheses sanitization work from
#37076 by consolidating both approaches into a unified
`sanitize_url_punctuation` function.

## Changes
- Combines parentheses and period sanitization into a single
`sanitize_url_punctuation` function
- Uses optimized single traversal with `fold()` for parentheses counting
(addressing code review feedback)
- Removes trailing periods using heuristics to distinguish sentence
punctuation from legitimate URL components
- Removes multiple trailing periods (always considered punctuation)
- Removes single trailing periods when they appear after alphanumeric
characters or slashes
- Preserves periods that are part of legitimate URL structure (e.g.,
version numbers, IP addresses, subdomains)
- Maintains existing parentheses balancing logic from #37076

## Implementation Details
- **Parentheses handling**: Counts opening and closing parentheses,
removes trailing `)` when unbalanced
- **Period handling**: Uses `take_while()` iterator for efficient period
counting
- **Performance**: Single pass counting with optimized loop to avoid
redundant work
- **Code clarity**: Uses let-else pattern for readable conditional logic

## Testing
- Added comprehensive test coverage for both parentheses and period
sanitization
- Tests cover balanced vs unbalanced parentheses cases  
- Tests cover various period scenarios including legitimate URL periods
vs sentence punctuation
- All existing tests continue to pass

## Release Notes

- Improved terminal URL detection by further trimming trailing
punctuation. URLs ending with periods (like
`https://example.com.`) and unbalanced parentheses (like
`https://example.com/path)`) are now properly detected without including
the trailing punctuation.
2025-09-09 10:39:09 -04:00
Dave Waggoner
af1875f91c terminal_view: Improve path hyperlink navigation by considering the terminal working directory (#36962)
Closes #34027

Release Notes:

- Improved terminal path hyperlink navigation by considering the
terminal working directory
2025-09-09 10:31:37 -04:00
Smit Barmase
734f94b71c agent_ui: Fix crash when typing multibyte character after mention (#37847)
Closes #36333

Release Notes:

- Fixed a crash that occurred when typing an IME character right after a
mention in the Agent Panel.
2025-09-09 19:16:22 +05:30
Danilo Leal
136468a4df keymap editor: Add some adjustments to the UI (#37819)
- Makes the keymap editor search container more consistent with the
project & file search corresponding elements
- Changes the keymap editor menu item in the user menu be called "Keymap
Editor", as opposed to "Key Binding", to match with the tab and action
name

Design note: Still a bit unsure about the extra space on the right for
the keymap editor. This makes it way more consistent with the other
search views, but it also just feels like space that could be used. On
the other hand, though, it's very unlikely anyone will ever use more
than 30% of the search bar width as search queries here are likely
pretty short; definitely much shorter than project search queries.

<img width="600" height="552" alt="Screenshot 2025-09-09 at 1  02@2x"
src="https://github.com/user-attachments/assets/9825a129-2c5a-4852-9837-c586b88e9332"
/>


Release Notes:

- N/A
2025-09-09 09:36:12 -03:00
Lukas Wirth
adf43d691a project: Remove non searchable buffer entries on buffer close (#37841)
Release Notes:

- N/A
2025-09-09 11:21:51 +00:00
localcc
466a2e22d5 Improve font rendering on macOS (#37622)
Part of https://github.com/zed-industries/zed/issues/7992

Release Notes:

- N/A
2025-09-09 13:46:59 +03:00
Finn Evers
365c5ab45f editor: Remove unnecessary clone (#37833)
The style is taken by reference everywhere, so no need to clone it at
the start of every `prepaint`.

Release Notes:

- N/A
2025-09-09 08:04:27 +00:00
Jakub Konka
11d81b95d4 Revert "git: Use self.git_binary_path instead raw git string" (#37828)
Reverts zed-industries/zed#37757
2025-09-09 06:36:25 +00:00
Michael Sloan
4b3b2acf75 Fix hot reload of builtin TreeSitter queries on Linux (#37825)
`fs.watch` is recursive on mac and non-recursive on Linux.

Release Notes:

- N/A
2025-09-09 05:55:28 +00:00
Danilo Leal
849424740f Fix code action menu items font size in toolbar (#37824)
Closes https://github.com/zed-industries/zed/issues/36478

Release Notes:

- N/A
2025-09-09 02:20:23 -03:00
Danilo Leal
3e605c2c4b docs: Fix casing on mentions to some brand names (#37822)
- MacOS → macOS
- VSCode → VS Code
- SublimeText → Sublime Text
- Javascript/Typescript → JavaScript/TypeScript

Release Notes:

- N/A
2025-09-09 01:45:55 -03:00
Danilo Leal
82b11bf77c docs: Include Cursor in the list of supported base keymaps (#37821)
We were missing that in the /key-bindings page. Also took advantage of
the opportunity to add a bunch of small writing tweaks.

Release Notes:

- N/A
2025-09-09 01:39:52 -03:00
Conrad Irwin
3a437fd888 Remove Chat (#37789)
At RustConf we were demo'ing zed, and it continually popped open the
chat panel.

We're usually inured to this because the Chat panel doesn't open unless
a Guest
is in the channel, but it made me sad that we were showing a long stream
of
vacuous comments and unresponded to questions on every demo screen.

We may bring chat back in the future, but we need more thought on the
UX, and
we need to rebuild the backend to not use the existing collab server
that we're
trying to move off of.

Release Notes:

- Removed the chat feature from Zed (Sorry to the 5 of you who use this
on the regular!)
2025-09-08 21:53:17 -06:00
Conrad Irwin
96c429d2c3 Only reject agent actions, don't restore checkpoint on revert (#37801)
Updates #37623

Release Notes:

- Changed the behaviour when editing an old message in a native agent
thread.
Prior to this, it would automatically restore the checkpoint (which
could
lead to a surprising amount of work being discarded). Now it will just
reject
any unaccepted agent edits, and you can use the "restore checkpoint"
button
  for the original behavior.
2025-09-08 20:18:40 -06:00
Marshall Bowers
ea4073e50e cloud_llm_client: Add new Plan variants (#37810)
This PR adds new variants to the `Plan` enum.

Release Notes:

- N/A
2025-09-09 00:18:43 +00:00
Ben Kunkle
8c93112869 settings_ui: Add Basic Implementation of Language Settings (#37803)
Closes #ISSUE

Release Notes:

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

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-09-08 20:14:36 -04:00
Marshall Bowers
1feffad5e8 Remove zed-pro feature flag (#37807)
This PR removes the `zed-pro` feature flag, as it was not being used.

Release Notes:

- N/A
2025-09-08 23:23:16 +00:00
Ivan Danov
ae54a4e1b8 Add commands to select next/previous siblings in the syntax tree (#35053)
Closes #5133 and discussion
https://github.com/zed-industries/zed/discussions/33493

This PR adds two new commands to select next/previous siblings in the
syntax tree. These commands were modelled after the existing ones about
expand/shrink selection. With this PR I've added new key bindings
inspired by `helix` for previous / next / expand / shrink selections.



https://github.com/user-attachments/assets/4ef7fadb-0b82-4897-95c7-1737827bf4ac


Release Notes:

- Add commands to select next/previous siblings in the syntax tree

---------

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-09-08 23:11:53 +00:00
Danilo Leal
4a0a7d1d27 Add item for the debugger panel in the app view menu (#37805)
Release Notes:

- Enabled the debugger panel to be opened via the app's "View" menu
option
2025-09-08 19:19:10 -03:00
Danilo Leal
5934d3789b python: Improve Basedpyright banner styles (#37802)
Just tidying this up a bit.

Release Notes:

- N/A
2025-09-08 18:51:13 -03:00
Danilo Leal
acde79dae7 agent: Improve popover trigger styles (#37800)
This PR mostly adds some style treatment to popover button triggers in
the agent panel, either making them better aligned with their trigger or
adjusting the color to better clarify which button is triggering the
currently opened menu.

Moving forward, I think the selected styles at least should probably be
tackled at the component level, whether that's a context menu or a
popover, so we don't have to ever do this manually (and running the risk
of forgetting to do it).

Release Notes:

- N/A
2025-09-08 18:39:49 -03:00
Patsakula Nikita
246c644316 agent_servers: Fix proxy configuration for Gemini (#37790)
Closes #37487 

Proxy settings are now taken from the Zed configuration and passed to
Gemini via the "--proxy" flag.

Release Notes:

- acp: Gemini ACP server now uses proxy settings from Zed configuration.
2025-09-08 20:44:40 +00:00
Marshall Bowers
e4de26e5dc cloud_llm_client: Remove unused code (#37799)
This PR removes some unused code from the `cloud_llm_client`.

This was only used on the server, so we can move it there.

Release Notes:

- N/A
2025-09-08 20:42:42 +00:00
ZhangJun
7091c70a1e open_ai: Trim newline before "data:" prefix and account for the possibility of no space after ":" (#37644)
I'am using an openai compatible model, but got nothing in agent thread
panel, and Zed log has "Model generated an empty summary" line.

I add one log to open_ai.rs:
<img width="2454" height="626" alt="图片"
src="https://github.com/user-attachments/assets/85354c7d-a0cc-4bba-86fd-2a640038a13e"
/>

and got:

<img width="3456" height="278" alt="图片"
src="https://github.com/user-attachments/assets/7746aedd-5d76-44b5-90f2-e129a1507178"
/>

It appear that `let line = line.strip_prefix("data: ")?;` can not handle
correctly.

Release Notes:

- N/A

---------

Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-08 22:01:55 +02:00
Cole Miller
fa0df6da1c python: Replace pyright with basedpyright (#35362)
Follow-up to #35250. Let's experiment with having this by default on
nightly.

Release Notes:

- Added built-in support for the basedpyright language server for Python
code. basedpyright is now enabled by default, and pyright (previously
the primary Python language server) remains available but is disabled by
default. This supersedes the basedpyright extension, which can be
uninstalled. Advantages of basedpyright over pyright include support for
inlay hints, semantic highlighting, auto-import code actions, and
stricter type checking. To switch back to pyright, add the following
configuration to settings.json:

```json
{
  "languages": {
    "Python": {
      "language_servers": ["pyright", "pylsp", "!basedpyright"]
    }
  }
}
```

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-09-08 19:15:17 +00:00
Cole Miller
99102a84fa ACP over SSH (#37725)
This PR adds support for using external agents in SSH projects via ACP,
including automatic installation of Gemini CLI and Claude Code,
authentication with API keys (for Gemini) and CLI login, and custom
agents from user configuration.

Co-authored-by: maan2003 <manmeetmann2003@gmail.com>

Release Notes:

- agent: Gemini CLI, Claude Code, and custom external agents can now be
used in SSH projects.

---------

Co-authored-by: maan2003 <manmeetmann2003@gmail.com>
2025-09-08 14:19:41 -04:00
Cole Miller
5f01f6d75f agent: Make read_file and edit_file tool call titles more specific (#37639)
For read_file and edit_file, show the worktree-relative path if there's
only one visible worktree, and the "full path" otherwise. Also restores
the display of line numbers for read_file calls.

Release Notes:

- N/A
2025-09-08 12:57:22 -04:00
Dave Waggoner
a66cd820b3 Fix line endings in terminal_hyperlinks.rs (#37654)
Fixes Windows line endings in `terminal_hyperlinks.rs`, which was
accidentally originally added with them.

Release Notes:

- N/A
2025-09-08 12:21:47 -04:00
hong jihwan
f07da9d9f2 Correctly parse backslash character on replacement (#37014)
When a keybind contains a backslash character (\\), it is parsed
incorrectly, which results in an invalid keybind configuration.


This patch fixes the issue by ensuring that backslashes are properly
escaped during the parsing process. This allows them to be used as
intended in keybind definitions.

Release Notes:

- Fixed an issue where keybinds containing a backslash character (\\)
failed to be replaced correctly


## Screenshots
<img width="912" height="530" alt="SCR-20250828-borp"
src="https://github.com/user-attachments/assets/561a040f-575b-4222-ac75-17ab4fa71d07"
/>
<img width="912" height="530" alt="SCR-20250828-bosx"
src="https://github.com/user-attachments/assets/b8e0fb99-549e-4fc9-8609-9b9aa2004656"
/>
2025-09-08 12:17:48 -04:00
Iha Shin (신의하)
8d05bb090c Add injections for Isograph function calls in JavaScript and TypeScript (#36320)
Required for https://github.com/isographlabs/isograph/pull/568 to work
properly. Tested with a local build and made sure everything's working
great!

Release Notes:

- JavaScript/TypeScript/JSX: Added support for injecting Isograph language support into `iso`
function calls
2025-09-08 16:04:37 +00:00
Dino
2325f14713 diagnostics: Current file diagnostics view (#34430)
These changes introduce a new command to the Diagnostics panel,
`diagnostics: deploy current file`, which allows the user to view the
diagnostics only for the currently opened file.

Here's a screen recording showing these changes in action 🔽 

[diagnostics: deploy current
file](https://github.com/user-attachments/assets/b0e87eea-3b3a-4888-95f8-9e21aff8ea97)

Closes #4739 

Release Notes:

- Added new `diagnostics: deploy current file` command to view
diagnostics for the currently open file

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-08 09:14:24 -06:00
Ben Kunkle
fe2aa3f4cb onboarding: Fix font loading frame delay (#37668)
Closes #ISSUE

Fixed an issue where the first frame of the `Editing` page in onboarding
would have a slight delay before rendering the first time it was
navigated to. This was caused by listing the OS fonts on the main
thread, blocking rendering. This PR fixes the issue by adding a new
method to the font family cache to prefill the cache on a background
thread.

Release Notes:

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

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
2025-09-08 11:09:54 -04:00
fantacell
10989c702c helix: Add match operator (#34060)
This is an implementation of matching like "m i (", as well as "] (" and
"[ (" in `helix_mode` with a few supported objects and a basis for more.

Release Notes:

- Added helix operators for selecting text objects

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-08 08:48:47 -06:00
张小白
3f80ac0127 macos: Fix menu bar flickering (#37707)
Closes #37526

Release Notes:

- Fixed menu bar flickering when using some IMEs on macOS.
2025-09-08 10:44:19 -04:00
Bennet Bo Fenner
4f1634f95c Remove unused semantic_index crate (#37780)
Release Notes:

- N/A
2025-09-08 13:38:31 +00:00
Eduardo Alba
40eec32cb8 markdown_preview: Fix trimming of leading whitespace in Markdown lists (#35750)
Closes #35712

Release Notes:

- Fixed white-space trimming leading to disconnect between list items
and content in markdown previews.

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-08 12:37:11 +00:00
张小白
17499453f6 windows: Check required GPU/driver feature StructuredBuffer (#37776)
Check whether the GPU/driver supports the StructuredBuffer feature
required by our shaders. If it doesn’t, log an error and skip that
GPU/driver, so Windows can fall back to the software renderer.

Release Notes:

- N/A
2025-09-08 12:22:57 +00:00
Lukas Wirth
80a4746a46 project: Be explicit about project-searchability for buffers (#37773)
Closes https://github.com/zed-industries/zed/issues/28830

Release Notes:

- Fixed builtin buffers and log views showing up in project search
2025-09-08 11:22:36 +00:00
Jakub Konka
01f5b73e3b cargo: Remove unused -fuse-ld=lld flag from Win config (#37769)
It is unused and generates a warning

```
 LINK : warning LNK4044: unrecognized option '/fuse-ld=lld'; ignored
```

If in the future we want to give `lld-link.exe` a try, we can set

```toml
linker = "lld-link.exe"
```

instead. At the time of writing, my tests have shown that there is no
real difference between `lld-link` and `link` in terms of linking speed.

Release Notes:

- N/A
2025-09-08 10:43:56 +00:00
Lukas Wirth
a0081dd693 project: Consider all worktrees for activation script search (#37764)
Should fix https://github.com/zed-industries/zed/issues/37734

Release Notes:

- Fixed venv not always activating correctly
2025-09-08 10:06:43 +00:00
chbk
f522823988 Highlight shorthand fields in Rust (#37674)
Release Notes:

- Highlight shorthand fields in Rust

| Zed 0.202.7 | With this PR |
| --- | --- |
| <img width="370" height="50" alt="rust-0 202 7"
src="https://github.com/user-attachments/assets/856a4d82-3ad0-4248-ad51-0472a0b6531a"
/> | <img width="370" height="50" alt="rust-pr"
src="https://github.com/user-attachments/assets/25b8e357-8519-4533-9026-3f2874b42ddb"
/> |
2025-09-08 12:32:08 +03:00
Martin Pool
5a8603bebb Install mold from the OS on Debian 13 (Trixie) (#37712)
I ran `scripts/linux` on Debian Trixie 13. It suggested manually
installing Mold, but [mold](http://packages.debian.org/mold) is packaged
on Debian and so we could install it automatically.

The version packaged there seems to work well enough for `cargo t` to
pass, at least.

## Tested

```
; sudo apt remove mold libstdc++-14-dev
... uninstalls them
; ./script/linux
The following NEW packages will be installed:
  build-essential clang clang-19 clang-tools-19 g++ g++-14 g++-14-x86-64-linux-gnu g++-x86-64-linux-gnu libstdc++-14-dev mold
; cargo t
(passes)
```

Release Notes:

- N/A
2025-09-08 12:29:17 +03:00
Martin Pool
abac87c2f8 tests: Fix doctests in crates/component (#37716)
Previously, `cargo test --package component` failed due to missing
imports for a doctest:


```

   Doc-tests component

running 1 test
test crates/component/src/component.rs - Component::description (line 229) ... FAILED

failures:

---- crates/component/src/component.rs - Component::description (line 229) stdout ----
error: cannot find derive macro `Documented` in this scope
 --> crates/component/src/component.rs:231:10
  |
4 | #[derive(Documented)]
  |          ^^^^^^^^^^

error[E0599]: no associated item named `DOCS` found for struct `MyComponent` in the current scope
 --> crates/component/src/component.rs:236:20
  |
5 | struct MyComponent;
  | ------------------ associated item `DOCS` not found for this struct
...
9 |         Some(Self::DOCS)
  |                    ^^^^ associated item not found in `MyComponent`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0599`.
Couldn't compile the test.

failures:
    crates/component/src/component.rs - Component::description (line 229)

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.29s

error: doctest failed, to rerun pass `-p component --doc`
bobcat ~/src/zed (doctests) 18:33

``` 

This might be unnoticed if you mostly run nextest, as it does not run
doctests.

Release Notes:

- N/A
2025-09-08 12:23:33 +03:00
Jakub Konka
c3d065cecc git: Use self.git_binary_path instead raw git string (#37757)
Release Notes:

- N/A
2025-09-08 11:05:04 +02:00
Remco Smits
e1a5d29972 markdown: Add support for inline HTML img tags inside text (#37264)
Follow-up: #36700

This PR adds basic support for showing images inline inside a text.

As you can see inside the before screenshot, the image was displayed
right below the `Some inline text` text. This was because we didn't
consider the image to be inline with the text (paragraph). Now we do :)

All the test changes are making sure it is not more than 1 element
parsed, instead of only checking for the first parsed element. This
could work out bad when we return more than 1 result.

**Before**
<img width="1717" height="1344" alt="Screenshot 2025-08-31 at 13 49 45"
src="https://github.com/user-attachments/assets/13c5f9dd-0e0a-4e08-b2a6-28e9a4e0cab8"
/>

**After**
<img width="1719" height="1343" alt="Screenshot 2025-08-31 at 13 42 14"
src="https://github.com/user-attachments/assets/bf7aa82f-3743-4fb3-87aa-4a97a550c4d1"
/>


**Code example**:
```markdown
<p>some inline text <img src="https://picsum.photos/200/300" alt="Description of image" style="height: 100px" /> asdjkflsadjfl</p>

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

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

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

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

Release Notes:

- Markdown: Added support for inline HTML `img` tags inside paragraphs
2025-09-08 11:49:32 +03:00
HE7086
d342da4e9a docs: Fix typos in language configurations (#37740)
Fixes several typos in the docs. I think there are more but I have only
read what I actually needed :)

Release Notes:

- N/A
2025-09-08 08:44:46 +00:00
Umesh Yadav
7ae8f81d74 language_models: Clear cached credentials when OpenAI and OpenAI Compatible provider api_url change (#37610)
Closes #37093

Also check this: #37099.

So currently in zed for both OpenAI and OpenAI Compatible provider when
the url is changed from settings the api_key stored in the provider
state is not cleared and it is still used. But if you restart zed the
api_key is cleared. Currently zed uses the api_url to store and fetch
the api key from credential provider. The behaviour is not changed
overall, it's just that we have made it consistent it with the zed
restart logic where it re-authenticates and fetches the api_key again. I
have attached the video below to show case before and after of this.

So all in all the problem was we were not re-authenticating the in case
api_url change while zed is still running. Now we trigger a
re-authentication and clear the state in case authentication fails.
 
OpenAI Compatible Provider:

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/324d2707-ea72-4119-8981-6b596a9f40a3"
/> | <video
src="https://github.com/user-attachments/assets/cc7fdb73-8975-4aaf-a642-809bb03ce319"
/> |

OpenAI Provider:

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/a1c07d1b-1909-4b49-b33c-fc05123e92e7"
/> | <video
src="https://github.com/user-attachments/assets/d78aeccd-5cd3-4d0c-8b9f-6f98e499d7c8"
/> |

Release Notes:

- Fixed OpenAI and OpenAI Compatible provide API keys being persisted
when changing the API URL setting. Authentication is now properly
revalidated when settings change.

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-08 06:57:16 +02:00
Umesh Yadav
36364b16a0 agent_ui: Clear model selector query on dismiss (#37569)
Closes #36756

| Before | After |
|--------|--------|
| <video src
="https://github.com/user-attachments/assets/1d022ac6-0aea-4e98-a717-9988420c9683"/>
| <video
src="https://github.com/user-attachments/assets/78d19012-1224-4c92-a6c8-47ae4c13ca31"/>
|

Release Notes:

- agent: Clear model selector query on dismiss in agent panel

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-08 06:52:57 +02:00
Umesh Yadav
b35959f4c2 agent_ui: Fix context_server duplication when name is updated (#35403)
Closes #35400

| Before | After |
|--------|--------|
| <video
src="https://github.com/user-attachments/assets/6aae34ca-e022-457a-9b66-47b85c976b23"/>
| <video
src="https://github.com/user-attachments/assets/ae058988-8f70-4605-b537-e045175d2e75"
/> |

Release Notes:

- agent: Fix `context_server` duplication when name is updated

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-09-08 04:50:26 +00:00
marius851000
9450bcad25 ollama: Properly format tool calls fed back to the model (#34750)
Fix an issue that resulted in Ollama models not being able to not being
able to access the input of the commands they executed (only being able
to access the result).

This properly return the function history as shown in
https://github.com/ollama/ollama/blob/main/docs/api.md#chat-request-with-history-with-tools

Previously, function input where not returned and result where returned
as a "user" role.

Release Notes:

- ollama: Improved format when returning tool results to the models
2025-09-08 04:26:01 +00:00
Liu Jinyi
69bdef38ec editor: Fix inconsistent search behavior for untitled/temporary tabs (#37086)
Closes #37597 

Release Notes:

- N/A

---


## Problem

When using "Tab Switcher: Toggle All", temporary files (untitled buffers
without associated file paths) cannot be searched by their displayed
content. This creates an inconsistent user experience where:

- **UI Display**: Shows dynamic titles based on the first line of
content (up to 40 characters)
- **Search Text**: Only searches for the static text "untitled"

### Example
- A temporary file containing `Hello World` is displayed as "Hello
World" in the tab
- However, searching for "Hello" in Tab Switcher returns no results
- Only searching for "untitled" will find this temporary file

## Root Cause

The issue stems from inconsistent title generation logic between display
and search:

1. **Display Title** (`items.rs:724`): Uses `self.title(cx)` →
`MultiBuffer::title()` → `buffer_content_title()`
- Returns the first line of content (max 40 chars) for temporary files
   
2. **Search Text** (`items.rs:650-656`): Uses `tab_content_text()`
method
   - Returns hardcoded "untitled" for files without paths

## Solution

Modified the `tab_content_text()` method in `crates/editor/src/items.rs`
to use the same logic as the displayed title for consistency:

```rust
fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
    if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
        path.to_string_lossy().to_string().into()
    } else {
        // Use the same logic as the displayed title for consistency
        self.buffer.read(cx).title(cx).to_string().into()
    }
}
```
2025-09-07 11:33:17 -07:00
Michael Sloan
0e33a3afe0 zeta: Check whether data collection is allowed for recent edit history (#37680)
Also:

* Adds tests for can_collect_data.
* Temporarily removes collection of diagnostics.

Release Notes:

- Edit Prediction: Fixed a bug where requests were marked eligible for
data collection despite the recent edit history in the request involving
files that may not be open source. The requests affected by this bug
will not be used in training data.
2025-09-07 11:16:49 -06:00
Bruno Taschenbier
76aaf6a8fe Fix docs for tabs.close_position in default.json (#37729)
Minor docs fix.
Seems like 0a4ff2f475 accidentally added
"hidden" to the docs of both – `close_position` and `show_close_button`.

Release Notes:

- N/A

Co-authored-by: tastenbier <>
2025-09-07 17:00:58 +00:00
张小白
0ef7ee172f windows: Remove some unused keys from the keymap (#37722)
AFAIK, we dont handle these keys on Windows.

Release Notes:

- N/A
2025-09-07 06:45:41 +00:00
张小白
29def012a1 windows: Update Windows keymap (#37721)
Pickup the changes from #37009

Release Notes:

- N/A
2025-09-07 06:09:35 +00:00
Smit Barmase
5c30578c49 linux: Fix IME preedit text not showing in Terminal on Wayland (#37701)
Closes https://github.com/zed-industries/zed/issues/37268
 
Release Notes:

- Fixed an issue where IME preedit text was not showing in the Terminal
on Wayland.
2025-09-07 02:01:55 +05:30
Cole Miller
1552afd8bf docs: Use #action throughout configuring-zed.md (#37709)
Release Notes:

- N/A
2025-09-06 19:38:48 +00:00
Kirill Bulatov
e04473dd26 Revert "gpui: Skip test attribute expansion for rust-analyzer (#37611)" (#37705)
This reverts commit 4124bedab7.

With the new annotation, r-a starts to skip the tasks that are marked
with `gpui::test` and when it fully loads, it starts to return
module-only tasks:


https://github.com/user-attachments/assets/5af3e3e4-91b7-4f19-aab0-ed7f186e5f74


Release Notes:

- N/A
2025-09-06 16:51:51 +00:00
Kirill Bulatov
84f166fc85 Tweak word completions more (#37697)
Follow-up of https://github.com/zed-industries/zed/pull/37352
Closes https://github.com/zed-industries/zed/issues/37132

* disabled word completions in the agent panel's editor
* if not disabled, allow to trigger word completions with an action even
if the completions threshold is not reached

Release Notes:

- Fixed word completions appearing in the agent panel's editor and not
appearing when triggered with the action before the completion threshold
is reached
2025-09-06 13:39:21 +00:00
Kirill Bulatov
065518577e Fix the tasks docs (#37699)
Closes https://github.com/zed-industries/zed/issues/37698

Release Notes:

- N/A
2025-09-06 13:37:21 +00:00
Marco Groot
1d828b6ac6 Fix broken link in CONTRIBUTING.md (#37688)
Can see currently the link is dead currently, but this changes fixes
locally


https://github.com/user-attachments/assets/e01d9c47-e91e-4c24-8285-01e3b45583b9


Release Notes:

- N/A
2025-09-06 12:29:34 +02:00
Kirill Bulatov
777ce7cc97 Fixed LSP binary info not being shown in full (#37682)
Follow-up of https://github.com/zed-industries/zed/pull/37083
Closes https://github.com/zed-industries/zed/issues/37677

Release Notes:

- Fixed LSP binary info not being shown in full
2025-09-06 07:37:59 +00:00
Umesh Yadav
1f37fbd051 language_models: Use /models/user for fetching OpenRouter models (#37534)
This PR switches the OpenRouter integration from fetching all models to
fetching only the models specified in the user's account preferences.
This will help improve the experience

**The Problem**

The previous implementation used the `/models` endpoint, which returned
an exhaustive list of all models supported by OpenRouter. This resulted
in a long and cluttered model selection dropdown in Zed, making it
difficult for users to find the models they actually use.

**The Solution**

We now use the `/models/user` endpoint. This API call returns a curated
list based on the models and providers the user has selected in their
[OpenRouter dashboard](https://openrouter.ai/models).

Ref: [OpenRouter API Docs for User-Filtered
Models](https://openrouter.ai/docs/api-reference/list-models-filtered-by-user-provider-preferences)

Release Notes:

- language_models: Support OpenRouter user preferences for available
models
2025-09-06 07:42:15 +02:00
Umesh Yadav
8c9442ad11 language_models: Skip empty delta text content in OpenAI and OpenAI compatible provider (#37626)
Closes #37302 

Related: #37614 

In case of open_ai_compatible providers like Zhipu AI and z.ai they
return empty content along with usage data. below is the example json
captured from z.ai. We now ignore empty content returned by providers
now to avoid this issue where we would return the same empty content
back to provider which would error out.

```
OpenAI Stream Response JSON:
{
  "id": "2025090518465610d80dc21e66426d",
  "created": 1757069216,
  "model": "glm-4.5",
  "choices": [
    {
      "index": 0,
      "finish_reason": "tool_calls",
      "delta": {
        "role": "assistant",
        "content": ""
      }
    }
  ],
  "usage": {
    "prompt_tokens": 7882,
    "completion_tokens": 150,
    "total_tokens": 8032,
    "prompt_tokens_details": {
      "cached_tokens": 7881
    }
  }
}
```

Release Notes:

- Skip empty delta text content in OpenAI and OpenAI compatible provider

Signed-off-by: Umesh Yadav <git@umesh.dev>
2025-09-06 07:16:08 +02:00
Michael Sloan
47a475681f Optimize Chunks::seek when offset is in current chunk (#37659)
Release Notes:

- N/A
2025-09-06 04:22:55 +00:00
Max Brunsfeld
23dc1f5ea4 Disable foreign keys in sqlite when running migrations (#37572)
Closes #37473

### Background

Previously, we enabled foreign keys at all times for our sqlite database
that we use for client-side state.
The problem with this is that In sqlite, `alter table` is somewhat
limited, so for many migrations, you must *recreate* the table: create a
new table called e.g. `workspace__2`, then copy all of the data from
`workspaces` into `workspace__2`, then delete the old `workspaces` table
and rename `workspaces__2` to `workspaces`. The way foreign keys work in
sqlite, when we delete the old table, all of its associated records in
other tables will be deleted due to `on delete cascade` clauses.

Unfortunately, one of the types of associated records that can be
deleted are `editors`, which sometimes store unsaved text. It is very
bad to delete these records, as they are the *only* place that this
unsaved text is stored.

This has already happened multiple times as we have migrated tables as
we develop Zed, but I caused it to happened again in
https://github.com/zed-industries/zed/pull/36714.

### The Fix

The Sqlite docs recommend a multi-step approach to migrations where you:

* disable foreign keys
* start a transaction
* create a new table
* populate the new table with data from the old table
* delete the old table
* rename the new table to the old name
* run a foreign key check
* if it passes, commit the transaction
* enable foreign keys

In this PR, I've adjusted our sqlite migration code path to follow this
pattern more closely. Specifically, we disable foreign key checks before
running migrations, run a foreign key check before committing, and then
enable foreign key checks after the migrations are done.

In addition, I've added a generic query that we run *before* running the
foreign key check that explicitly deletes any rows that have dangling
foreign keys. This way, we avoid failing the migration (and breaking the
app) if a migration deletes data that *does* cause associated records to
need to be deleted.

But now, in the common case where we migrate old data in the new table
and keep the ids, all of the associated data will be preserved.

Release Notes:

- Fixed a bug where workspace state would be lost when upgrading from
Zed 0.201.x. or below.
2025-09-06 01:09:50 +00:00
chbk
a6a111cadd Highlight labels in Go (#37673)
Release Notes:

- Highlight labels in Go

| Zed 0.202.7 | With this PR |
| --- | --- |
| <img width="160" height="50" alt="go-0 202 7"
src="https://github.com/user-attachments/assets/1a1b3b3c-52ae-41e3-a52b-c2a9bb7589d2"
/> | <img width="160" height="50" alt="go-pr"
src="https://github.com/user-attachments/assets/29020b9f-ed03-4298-aa5b-0201a81fd5e6"
/> |
2025-09-06 01:36:36 +02:00
Piotr Osiewicz
6a7b84eb87 toolchains: Allow users to provide custom paths to toolchains (#37009)
- **toolchains: Add new state to toolchain selector**
- **Use toolchain term for Add Toolchain button**
- **Hoist out a meta function for toolchain listers**

Closes #27332

Release Notes:

- python: Users can now specify a custom path to their virtual
environment from within the picker.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-09-06 00:47:39 +02:00
Nia
59bdbf5a5d Various fixups to unsafe code (#37651)
A collection of fixups of possibly-unsound code and removing some small
useless writes.

Release Notes:

- N/A
2025-09-06 00:27:14 +02:00
Remco Smits
64b6e8ba0f debugger: Fix allow showing more than 1 compact session item (#37036)
Closes #36978

This PR fixes an issue that we would only show the first `root -> child`
session in compact mode, but the session that came after it, we would
only show the child session label instead of also adding the parent
label due to compact mode.

## Before
<img width="348" height="173" alt="Screenshot 2025-08-27 at 22 18 39"
src="https://github.com/user-attachments/assets/ad6afd3a-196d-497f-812a-00698676ee90"
/>

## After
<img width="563" height="211" alt="Screenshot 2025-08-27 at 21 57 16"
src="https://github.com/user-attachments/assets/a953ef2a-a796-4160-b868-96e96f81c858"
/>

With 3 parent + child sessions and one parent session only.
<img width="484" height="223" alt="Screenshot 2025-08-27 at 22 22 13"
src="https://github.com/user-attachments/assets/a26f79a4-63a5-43d0-a714-d62cb1995e6e"
/>

cc @cole-miller I know we hacked on this some while ago, so figured you
might be the best guy to ask for a review.

Release Notes:

- Debugger: Fix to allow showing more than 1 compact session item

---------

Co-authored-by: Anthony <anthony@zed.dev>
2025-09-05 21:35:28 +00:00
morgankrey
236b3e546e Update Link (#37671)
Documentation fix

Release Notes:

- N/A
2025-09-05 14:34:13 -07:00
Anthony Eid
ea363466aa Fix attach modal showing local processes in SSH sessions (#37608)
Closes #37520

This change makes the attach modal load processes from the remote server
when connecting via SSH, rather than showing local processes from the
client machine.

This works by using the new GetProcessesRequest RPC message to allow
downstream clients to get the correct processes to display. It also only
works with downstream ssh clients because the message handler is only
registered on headless projects.

Release Notes:

- debugger: Fix bug where SSH attach modal showed local processes
instead of processes from the server
2025-09-05 17:03:42 -04:00
Smit Barmase
c45177e296 editor: Fix fold placeholder hover width smaller than marker (#37663)
Bug:
<img width="196" height="95" alt="Screenshot 2025-09-06 at 1 21 39 AM"
src="https://github.com/user-attachments/assets/66ec0fc9-961e-4289-bd75-68b24dad485e"
/>

The fold marker we use, `⋯`, isn’t rendered at the same size as the
editor’s font. Notice how the fold marker appears larger than the same
character typed directly in the editor buffer.

<img width="146" height="82" alt="image"
src="https://github.com/user-attachments/assets/a059d221-6b55-4cf9-bc1e-898ff5444006"
/>

When we shape the line, we use the editor’s font size, and it ends up
determining the element’s width. To fix this, we should treat the
ellipsis as a UI element rather than a buffer character, since current
visual size looks good to me.

<img width="196" height="95" alt="Screenshot 2025-09-06 at 1 29 28 AM"
src="https://github.com/user-attachments/assets/1b766d46-00ab-40c7-b98a-95ea2d4b29bf"
/>

Release Notes:

- Fixed an issue where the fold placeholder’s hover area was smaller
than the marker.
2025-09-06 02:05:42 +05:30
Finn Evers
45fa034107 Restore notification panel settings (#37661)
Follow-up to https://github.com/zed-industries/zed/pull/37489

Notification panel settings were always missing the content, hence this
PR adds it. After #37489, the use of the same content twice broke
things, which currently makes the notification panel non-configurable on
Nightly. This PR fixes this.

There once was an issue about the documentation for the panel being
wrong as well. However, I was just unable to find that sadly.

Release Notes:

- N/A
2025-09-05 21:50:51 +02:00
Conrad Irwin
1c5c8552f2 Show actual error in InvalidBufferView (#37657)
Release Notes:

- Update error view to show the error
2025-09-05 12:03:26 -07:00
Conrad Irwin
5d374193bb Add terminal::Toggle (#37585)
Co-Authored-By: Brandan <b5@n0.computer>

Release Notes:

- Added a new action `terminal::Toggle` that is by default bound to
'ctrl-\`'. This copies the default behaviour from VSCode and Jetbrains
where the terminal opens and closes correctly. If you'd like the old
behaviour you can rebind 'ctrl-\`' to `terminal::ToggleFocus`

Co-authored-by: Brandan <b5@n0.computer>
2025-09-05 17:34:39 +00:00
Dino
b65fb06264 editor: Fix text manipulation on line mode selections (#37646)
This commit updates the implementation for
`editor::Editor.manipulate_text` to use
`editor::selections_collection::SelectionsCollection.all_adjusted`
instead of `editor::selections_collection::SelectionsCollection.all`, as
the former takes into account the selection's `line_mode`, fixing the
issue where, if an user was in vim's visual line mode, running the
`editor: convert to upper case` command would not work as expected.

Closes #36953 

Release Notes:

- Fixed bug where using the editor's convert case commands while in
vim's Visual Line mode would not work as expected
2025-09-05 10:12:51 -07:00
Matin Aniss
b3405c3bd1 Add line ending selector (#35392)
Partially addresses this issue #5294

Adds a selector between `LF` and `CRLF` for the buffer's line endings,
the checkmark denotes the currently selected line ending.

Selector
<img width="487" height="66" alt="image"
src="https://github.com/user-attachments/assets/13f2480f-4d2d-4afe-adf5-385aeb421393"
/>

Release Notes:

- Added line ending selector.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-09-05 09:52:57 -07:00
Peter Tripp
638320b21e Improve macOS version information in telemetry (#37185)
macOS versions are currently reported as `macOS 26.0.0`.
But this makes it impossible to differentiate amongst macOS Beta
releases which have the same version number (`X.0.0`) but are different
builds.

This PR adds build number info to `os_version` for macOS Betas and
[Rapid Security Response](https://support.apple.com/en-us/102657)
release that have identical version numbers to stable release, but have
different builds numbers. We can differentiate them because the build
numbers end with a letter.

| Version | Before | After |
| - | - | - | 
| macOS Sonoma 14.7.8 | 14.7.8 | 14.7.8 |
| macOS Sequoia 15.6.1 | 15.6.1 | 15.6.1 |
| mcOS Ventura 13.3.1 | 13.3.1 | 13.3.1 |
| macOS Ventura 13.3.1 (a) |  13.3.1 | 13.3.1 (Build 22E772610a) |
| macOS Tahoe 26.0.0 (Beta1) | 26.0.0 | 26.0.0 (Build 25A5316a) |
| macOS Tahoe 26.0.0 (Beta5) | 26.0.0 | 26.0.0 (Build 25A5349a) | 

This should cause minimal telemetry changes and only impacting a macOS
betas and a couple specific older macOS versions, but will allow
differentiation between macOS beta releases in GitHub issues.

Alternatives:
1. Leave as-is (can't differentiate between macOS beta builds)
2. Always include build number info (impacts telemetry; more consistent
going forward; differentiates non-final Release Candidates which don't
include a trailing letter)

I couldn't find a cocoa method to retrieve macOS build number, so I
switched dependencies from `cocoa` to `objc2-foundation` in the client
crate. We already depend upon this crate as a dependency of
`blade-graphics` so I matched the features of that and so workspace-hack
doesn't change.

1ebc69a447/tooling/workspace-hack/Cargo.toml (L355)

Release Notes:

- N/A
2025-09-05 12:40:47 -04:00
张小白
91ab0636ec windows: Make sure zed.sh using the correct line ending (#37650)
This got missed in the changes from #37631

Release Notes:

- N/A
2025-09-05 16:25:55 +00:00
Yacine Hmito
fb6cc8794f Fix typo in development docs for macOS (#37607)
Release Notes:

- N/A
2025-09-05 15:56:40 +00:00
Jakub Konka
3d37611b6f cli: Rename script zed-wsl to zed, and enable on non-WSL (#37631)
Closes #23026

With this hotfix, git committing from the built-in Zed terminal (well,
PowerShell), now works.

Release Notes:

- N/A
2025-09-05 17:43:39 +02:00
Peter Tripp
360e372b57 linux: Restore ctrl-escape to keymap (#37636)
Closes: https://github.com/zed-industries/zed/issues/37628
Follow-up to: https://github.com/zed-industries/zed/pull/36712

Release Notes:

- linux: Fix for ctrl-escape not escaping the tab switcher.
2025-09-05 11:09:32 -04:00
Isaac Hales
74e8afe9a8 Fix logic for default values for task variables (#37588)
This is a small fix for default values in task variables. The
[documentation](https://zed.dev/docs/tasks) states

> You can also use verbose syntax that allows specifying a default if a
given variable is not available: ${ZED_FILE:default_value}

I found, however, that this doesn't actually work. Instead, the Zed
variable and the default value are just appended in the output. For
example, if I run a task `echo ${ZED_ROW:100}` the result I get is
`447:100` (in this case it should just be `447`).

This PR fixes that. I also added a new test case for handling default
values.
I also tested the fix in a dev build and it seems to work.

There are no UI adjustments.

AI disclosure: I used Claude Code to write the code, including the fix
and the tests.

This is actually my first open-source PR ever, so if I did something
wrong, I'd appreciate any tips and I'll make it right!


Release Notes:

- Fixed task variable substitution always appending the default
2025-09-05 14:57:58 +00:00
Finn Evers
e30f45cf64 Syntax tree view improvements (#37570)
In an effort to improve the experience while developing extensions and
improving themes, this PR updates the syntax tree views behavior
slightly.

Before, the view would always update to the current active editor whilst
being used. This was quite painful for improving extension scheme files,
as you would always have to change back and forth between editors to
have a view at the relevant syntax tree.

With this PR, the syntax tree view will now stay attached to the editor
it was opened in, similar to preview views. Once the view is shown, the
`UseActiveEditor` will become available in the command palette and
enable the user to update the view to the last focused editor. On file
close, the view will also be updated accordingly.



https://github.com/user-attachments/assets/922075e5-9da0-4c1d-9e1a-51e024bf41ea

A button is also shown whenever switching is possible.

Futhermore, improved the empty state of the view.

Lastly, a drive-by cleanup of the `show_action_types` method so there is
no need to call `iter()` when calling the method.


Release Notes:

- The syntax tree view will now stay attached to the buffer it was
opened in, similar to the Markdown preview. Use the `UseActiveEditor`
action when the view is shown to change it to the last focused editor.
2025-09-05 14:22:32 +02:00
Jakub Konka
16c4fd4fc5 gpui: move Option -> Result conversion out of closure in App::update_window_id (#37624)
Doesn't fix anything, but it seems that we do not need to assert and
convert into an error until after the closure run to completion,
especially since this is the only error we throw.

Release Notes:

- N/A
2025-09-05 13:19:57 +02:00
Lukas Wirth
ec58adca13 languages: Invoke conda activate in conda environments (#37627)
This isn't quite right, but using the env manager path causes conda to
scream and I am not yet sure why, either way this is an improvement over
the status quo

Release Notes:

- N/A\
2025-09-05 11:16:15 +00:00
Lukas Wirth
bed358718b agent_ui: Fix index panic in SlashCommandCompletion::try_parse (#37612)
Release Notes:

- N/A
2025-09-05 07:56:53 +00:00
Lukas Wirth
4124bedab7 gpui: Skip test attribute expansion for rust-analyzer (#37611)
The `test` attribute doesn't really matter to rust-analyzer, so we can
make use of its cfg to have it think its just the standard test
attribute which should make rust-analyzer slightly less resource
intensive in zed. It also should prevent some IDE features from possibly
failing within tests.

Notably this has no effect outside of this repo, as the `rust-analyzer`
cfg only takes effect on workspace member crates.

Ideally we'd use the ignored proc macro config here but rust-analyzer
still doesn't have toml configs working unfortunately.

Release Notes:

- N/A
2025-09-05 06:54:08 +00:00
Smit Barmase
57c6dbd71e linux: Fix IME positioning on scaled display on Wayland (#37600)
Removes IME bounds scaling on Wayland since it uses logical pixels,
unlike X11. We now scale only on X11. Windows and macOS don’t use these
bounds for IME anyway.

Release Notes:

- Fixed an issue where the IME popover could appear outside the window
or fail to show on Wayland.
2025-09-05 09:10:50 +05:30
Michael Sloan
fded3fbcdb zeta: Scope edit prediction event history to current project (#37595)
This change also causes Zeta to not do anything for editors that are not
associated with a project. In practice, this shouldn't affect any
behavior - those editors shouldn't have edit predictions anyway.

Release Notes:

- Edit Prediction: Requests no longer include recent edits from other
projects (other Zed windows).
2025-09-05 01:15:59 +00:00
Michael Sloan
a660527036 Make entry_for_path return a reference instead of cloning (#37591)
Release Notes:

- N/A
2025-09-05 00:26:37 +00:00
Anthony Eid
0cb8a8983c settings ui: Improve setting proc macro and add scroll to UI (#37581)
This PR improves the settings_ui proc macro by taking into account more
serde attributes
1. rename_all
2. rename
3. flatten

We also pass field documentation to the UI layer now too. This allows ui
elements to have more information like the switch field description.

We got the scrollbar working and started getting language settings to
show up.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-09-04 22:30:48 +00:00
Cole Miller
c7902478c1 acp: Pass project environment to external agent servers (#37568)
Closes #37469 

Release Notes:

- agent: The project shell environment is now passed to external agent
processes.

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Nia Espera <nia-e@haecceity.cc>
2025-09-04 18:16:25 -04:00
morgankrey
3c0183fa5e Extraneous backtick (#37576)
Release Notes:

- N/A
2025-09-04 16:14:57 -05:00
morgankrey
e982cb824a docs: Claude Authentication (#37573)
Release Notes:

- N/A
2025-09-04 15:57:00 -05:00
Marshall Bowers
1b865a60f8 snippets: Bump to v0.0.6 (#37567)
This PR bumps the snippets extension to v0.0.6.

Changes:

- https://github.com/zed-industries/zed/pull/37565

Release Notes:

- N/A
2025-09-04 20:08:49 +00:00
Marshall Bowers
4c32d5bf13 snippets: Disable feature_paths by default (#37565)
This PR updates the default configuration of the `snippets` extension to
disable suggesting paths (`feature_paths`).

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

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

Release Notes:

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

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

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

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

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

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

Release Notes:

- Made fallback open picker more intuitive

---------

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

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

Release Notes:

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

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

Release Notes:

- N/A

---------

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



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


Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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


Release Notes:

- N/A

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

No reason to not use the full SHA.

Release Notes:

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

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

Closes #37448

Release Notes:

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

Release Notes:

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

---------

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

Release Notes:

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

## Changes

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

## Benefits

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

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

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


Release Notes:

- N/A

---------

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

---------

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

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

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

Release Notes:

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

Closes #37212

Release Notes:

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

Now the official API supports 128k token content

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

Release Notes:

- N/A

---------

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

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

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

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

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

Closes #37389

Release Notes:

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

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

Closes #37093

Release Notes:

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

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

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

Release Notes:

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

---------

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

Release Notes:

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

cc: @katie-z-geer 

Release Notes:

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

- N/A

---------

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

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

Release Notes:

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

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

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

Release Notes:

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



Release Notes:

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

---------

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

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

Release Notes:

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

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

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

Release Notes:

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

- N/A

---------

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

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

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

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

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

Release Notes:

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

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

Closes #35908

Release Notes:

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

---------

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

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

---------

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

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

### Second issue

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

---

This PR fixes both of these issues

Release Notes:

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

- N/A

---------

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

- N/A

---

Same case in HTML example:


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

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

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

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

### Before

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

### After 

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

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


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

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

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

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

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


Release Notes:

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

Related to #37213

Release Notes:

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

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

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

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

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

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

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

Release Notes:

- N/A

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

Closes #ISSUE

Release Notes:

- N/A

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

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

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

Release Notes:

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

Release Notes:

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

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

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



Release Notes:

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

Reverts #35926 

Release Notes:

- N/A

---------

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

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

Release Notes:

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

---------

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

Release Notes:

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

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

Release Notes:

- N/A

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

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

Show a clearer error here until SSH support is implemented.

Release Notes:

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

Release Notes:

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

Closes #35434 

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

Release Notes:

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

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

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

Release Notes:

- N/A

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

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

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

Release Notes:

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

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

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

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

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

Closes #24439

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

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

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

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


Release Notes:

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

Release Notes:

- Improve error messages for unsupported ACP slash-commands

---------

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

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

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

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

### Why this approach?

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


Release Notes:

- Improved tool compatibility with OpenAI API-compatible providers

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

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

Release Notes:

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

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


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


Release Notes:

- N/A

---------

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

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

Release Notes:

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

Release Notes:

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

---------

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

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

Release Notes:

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

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


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

Release Notes:

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

Release Notes:

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

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

For example, with the following string:

```
' str '
```

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

```
"str"
```

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

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

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

Closes #12247 

Release Notes:

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

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

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

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

Release Notes:

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

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


Release Notes:

- N/A

---------

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

Release Notes:

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

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

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

Release Notes:

- N/A

---------

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

Release Notes:

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

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

Release Notes:

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

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

Release Notes:

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

Closes #19794
Closes #18221

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

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

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

Release Notes:

- Added support for rewrap in block comments.

---------

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

Another case that could be addressed by Bors.

Release Notes:

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

- N/A

---------

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

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

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

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

Release Notes:

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


Release Notes:

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

Release Notes:

- Add support for custom API Urls for DeepSeek Provider

---------

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

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

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

#### Before/after

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

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

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

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

Release Notes:

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

H/T: @brandon-fryslie

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

### GitHub Vulnerability Alerts

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

### Impact

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

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

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

### Patches

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

### Workarounds

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

### References

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

### Acknowledgments

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

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

---

### Release Notes

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

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

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

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

#### Impact

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

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

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

#### Solution

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

#### Affected Versions

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

#### Recommendations

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

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

#### Migration

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

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

#### Acknowledgments

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

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

</details>

---

### Configuration

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

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

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

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

---

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

---

Release Notes:

- N/A

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

---------

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

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

Code example:

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

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

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

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

Release Notes:

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

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

- Fixed auto size rendering of SVG images in Markdown.

## Before

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


## After

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

For GPUI example

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

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

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

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

## Todo

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

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

Release Notes:

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

---------

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


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

Into the agent2 code.

Release Notes:

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

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

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

Release Notes:

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

That broke tests.

Release Notes:

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

Release Notes:

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

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

### Structure

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

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

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

### Foundation 

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

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


#### Immediate Follow ups

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

Release Notes:

- N/A

---------

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

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

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

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

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

### Recording


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

---

Closes #13194 

Release Notes:

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

---------

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

Release Notes:

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

How it works:

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

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

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


Result:

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

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

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

Release Notes:

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

- N/A

---------

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

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

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

Release Notes:

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

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

Release Notes:

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

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

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

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

Release Notes:

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

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

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

Release Notes:

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

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

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

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

Release Notes:

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

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

Release Notes:

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

Release Notes:

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

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

Release Notes:

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

It makes tab switching feel even faster!

Release Notes:

- N/A

Co-authored-by: David Kleingeld <davidsk@zed.dev>
2025-08-28 21:07:02 +00:00
675 changed files with 33113 additions and 28031 deletions

View File

@@ -19,8 +19,6 @@ rustflags = [
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
"-C",
"link-arg=-fuse-ld=lld",
]
[env]

View File

@@ -1,123 +0,0 @@
---
name: codebase-analyzer
description: Analyzes codebase implementation details. Call the codebase-analyzer agent when you need to find detailed information about specific components. As always, the more detailed your request prompt, the better! :)
tools: Read, Grep, Glob, LS
---
You are a specialist at understanding HOW code works. Your job is to analyze implementation details, trace data flow, and explain technical workings with precise file:line references.
## Core Responsibilities
1. **Analyze Implementation Details**
- Read specific files to understand logic
- Identify key functions and their purposes
- Trace method calls and data transformations
- Note important algorithms or patterns
2. **Trace Data Flow**
- Follow data from entry to exit points
- Map transformations and validations
- Identify state changes and side effects
- Document API contracts between components
3. **Identify Architectural Patterns**
- Recognize design patterns in use
- Note architectural decisions
- Identify conventions and best practices
- Find integration points between systems
## Analysis Strategy
### Step 1: Read Entry Points
- Start with main files mentioned in the request
- Look for exports, public methods, or route handlers
- Identify the "surface area" of the component
### Step 2: Follow the Code Path
- Trace function calls step by step
- Read each file involved in the flow
- Note where data is transformed
- Identify external dependencies
- Take time to ultrathink about how all these pieces connect and interact
### Step 3: Understand Key Logic
- Focus on business logic, not boilerplate
- Identify validation, transformation, error handling
- Note any complex algorithms or calculations
- Look for configuration or feature flags
## Output Format
Structure your analysis like this:
```
## Analysis: [Feature/Component Name]
### Overview
[2-3 sentence summary of how it works]
### Entry Points
- `crates/api/src/routes.rs:45` - POST /webhooks endpoint
- `crates/api/src/handlers/webhook.rs:12` - handle_webhook() function
### Core Implementation
#### 1. Request Validation (`crates/api/src/handlers/webhook.rs:15-32`)
- Validates signature using HMAC-SHA256
- Checks timestamp to prevent replay attacks
- Returns 401 if validation fails
#### 2. Data Processing (`crates/core/src/services/webhook_processor.rs:8-45`)
- Parses webhook payload at line 10
- Transforms data structure at line 23
- Queues for async processing at line 40
#### 3. State Management (`crates/storage/src/stores/webhook_store.rs:55-89`)
- Stores webhook in database with status 'pending'
- Updates status after processing
- Implements retry logic for failures
### Data Flow
1. Request arrives at `crates/api/src/routes.rs:45`
2. Routed to `crates/api/src/handlers/webhook.rs:12`
3. Validation at `crates/api/src/handlers/webhook.rs:15-32`
4. Processing at `crates/core/src/services/webhook_processor.rs:8`
5. Storage at `crates/storage/src/stores/webhook_store.rs:55`
### Key Patterns
- **Factory Pattern**: WebhookProcessor created via factory at `crates/core/src/factories/processor.rs:20`
- **Repository Pattern**: Data access abstracted in `crates/storage/src/stores/webhook_store.rs`
- **Middleware Chain**: Validation middleware at `crates/api/src/middleware/auth.rs:30`
### Configuration
- Webhook secret from `crates/config/src/webhooks.rs:5`
- Retry settings at `crates/config/src/webhooks.rs:12-18`
- Feature flags checked at `crates/common/src/utils/features.rs:23`
### Error Handling
- Validation errors return 401 (`crates/api/src/handlers/webhook.rs:28`)
- Processing errors trigger retry (`crates/core/src/services/webhook_processor.rs:52`)
- Failed webhooks logged to `logs/webhook-errors.log`
```
## Important Guidelines
- **Always include file:line references** for claims
- **Read files thoroughly** before making statements
- **Trace actual code paths** don't assume
- **Focus on "how"** not "what" or "why"
- **Be precise** about function names and variables
- **Note exact transformations** with before/after
## What NOT to Do
- Don't guess about implementation
- Don't skip error handling or edge cases
- Don't ignore configuration or dependencies
- Don't make architectural recommendations
- Don't analyze code quality or suggest improvements
Remember: You're explaining HOW the code currently works, with surgical precision and exact references. Help users understand the implementation as it exists today.

View File

@@ -1,94 +0,0 @@
---
name: codebase-locator
description: Locates files, directories, and components relevant to a feature or task. Call `codebase-locator` with human language prompt describing what you're looking for. Basically a "Super Grep/Glob/LS tool" — Use it if you find yourself desiring to use one of these tools more than once.
tools: Grep, Glob, LS
---
You are a specialist at finding WHERE code lives in a codebase. Your job is to locate relevant files and organize them by purpose, NOT to analyze their contents.
## Core Responsibilities
1. **Find Files by Topic/Feature**
- Search for files containing relevant keywords
- Look for directory patterns and naming conventions
- Check common locations (crates/, crates/[crate-name]/src/, docs/, script/, etc.)
2. **Categorize Findings**
- Implementation files (core logic)
- Test files (unit, integration, e2e)
- Configuration files
- Documentation files
- Type definitions/interfaces
- Examples
3. **Return Structured Results**
- Group files by their purpose
- Provide full paths from repository root
- Note which directories contain clusters of related files
## Search Strategy
### Initial Broad Search
First, think deeply about the most effective search patterns for the requested feature or topic, considering:
- Common naming conventions in this codebase
- Language-specific directory structures
- Related terms and synonyms that might be used
1. Start with using your grep tool for finding keywords.
2. Optionally, use glob for file patterns
3. LS and Glob your way to victory as well!
### Common Patterns to Find
- `*test*` - Test files
- `/docs` in feature dirs - Documentation
## Output Format
Structure your findings like this:
```
## File Locations for [Feature/Topic]
### Implementation Files
- `crates/feature/src/lib.rs` - Main crate library entry point
- `crates/feature/src/handlers/mod.rs` - Request handling logic
- `crates/feature/src/models.rs` - Data models and structs
### Test Files
- `crates/feature/src/tests.rs` - Unit tests
- `crates/feature/tests/integration_test.rs` - Integration tests
### Configuration
- `Cargo.toml` - Root workspace manifest
- `crates/feature/Cargo.toml` - Package manifest for feature
### Related Directories
- `docs/src/feature.md` - Feature documentation
### Entry Points
- `crates/zed/src/main.rs` - Uses feature module at line 23
- `crates/collab/src/main.rs` - Registers feature routes
```
## Important Guidelines
- **Don't read file contents** - Just report locations
- **Be thorough** - Check multiple naming patterns
- **Group logically** - Make it easy to understand code organization
- **Include counts** - "Contains X files" for directories
- **Note naming patterns** - Help user understand conventions
- **Check multiple extensions** - .rs, .md, .js/.ts, .py, .go, etc.
## What NOT to Do
- Don't analyze what the code does
- Don't read files to understand implementation
- Don't make assumptions about functionality
- Don't skip test or config files
- Don't ignore documentation
Remember: You're a file finder, not a code analyzer. Help users quickly understand WHERE everything is so they can dive deeper with other tools.

View File

@@ -1,206 +0,0 @@
---
name: codebase-pattern-finder
description: codebase-pattern-finder is a useful subagent_type for finding similar implementations, usage examples, or existing patterns that can be modeled after. It will give you concrete code examples based on what you're looking for! It's sorta like codebase-locator, but it will not only tell you the location of files, it will also give you code details!
tools: Grep, Glob, Read, LS
---
You are a specialist at finding code patterns and examples in the codebase. Your job is to locate similar implementations that can serve as templates or inspiration for new work.
## Core Responsibilities
1. **Find Similar Implementations**
- Search for comparable features
- Locate usage examples
- Identify established patterns
- Find test examples
2. **Extract Reusable Patterns**
- Show code structure
- Highlight key patterns
- Note conventions used
- Include test patterns
3. **Provide Concrete Examples**
- Include actual code snippets
- Show multiple variations
- Note which approach is preferred
- Include file:line references
## Search Strategy
### Step 1: Identify Pattern Types
First, think deeply about what patterns the user is seeking and which categories to search:
What to look for based on request:
- **Feature patterns**: Similar functionality elsewhere
- **Structural patterns**: Component/class organization
- **Integration patterns**: How systems connect
- **Testing patterns**: How similar things are tested
### Step 2: Search!
- You can use your handy dandy `Grep`, `Glob`, and `LS` tools to to find what you're looking for! You know how it's done!
### Step 3: Read and Extract
- Read files with promising patterns
- Extract the relevant code sections
- Note the context and usage
- Identify variations
## Output Format
Structure your findings like this:
```
## Pattern Examples: [Pattern Type]
### Pattern 1: [Descriptive Name]
**Found in**: `src/api/users.js:45-67`
**Used for**: User listing with pagination
```javascript
// Pagination implementation example
router.get('/users', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const offset = (page - 1) * limit;
const users = await db.users.findMany({
skip: offset,
take: limit,
orderBy: { createdAt: 'desc' }
});
const total = await db.users.count();
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total,
pages: Math.ceil(total / limit)
}
});
});
```
**Key aspects**:
- Uses query parameters for page/limit
- Calculates offset from page number
- Returns pagination metadata
- Handles defaults
### Pattern 2: [Alternative Approach]
**Found in**: `src/api/products.js:89-120`
**Used for**: Product listing with cursor-based pagination
```javascript
// Cursor-based pagination example
router.get('/products', async (req, res) => {
const { cursor, limit = 20 } = req.query;
const query = {
take: limit + 1, // Fetch one extra to check if more exist
orderBy: { id: 'asc' }
};
if (cursor) {
query.cursor = { id: cursor };
query.skip = 1; // Skip the cursor itself
}
const products = await db.products.findMany(query);
const hasMore = products.length > limit;
if (hasMore) products.pop(); // Remove the extra item
res.json({
data: products,
cursor: products[products.length - 1]?.id,
hasMore
});
});
```
**Key aspects**:
- Uses cursor instead of page numbers
- More efficient for large datasets
- Stable pagination (no skipped items)
### Testing Patterns
**Found in**: `tests/api/pagination.test.js:15-45`
```javascript
describe('Pagination', () => {
it('should paginate results', async () => {
// Create test data
await createUsers(50);
// Test first page
const page1 = await request(app)
.get('/users?page=1&limit=20')
.expect(200);
expect(page1.body.data).toHaveLength(20);
expect(page1.body.pagination.total).toBe(50);
expect(page1.body.pagination.pages).toBe(3);
});
});
```
### Which Pattern to Use?
- **Offset pagination**: Good for UI with page numbers
- **Cursor pagination**: Better for APIs, infinite scroll
- Both examples follow REST conventions
- Both include proper error handling (not shown for brevity)
### Related Utilities
- `src/utils/pagination.js:12` - Shared pagination helpers
- `src/middleware/validate.js:34` - Query parameter validation
```
## Pattern Categories to Search
### API Patterns
- Route structure
- Middleware usage
- Error handling
- Authentication
- Validation
- Pagination
### Data Patterns
- Database queries
- Caching strategies
- Data transformation
- Migration patterns
### Component Patterns
- File organization
- State management
- Event handling
- Lifecycle methods
- Hooks usage
### Testing Patterns
- Unit test structure
- Integration test setup
- Mock strategies
- Assertion patterns
## Important Guidelines
- **Show working code** - Not just snippets
- **Include context** - Where and why it's used
- **Multiple examples** - Show variations
- **Note best practices** - Which pattern is preferred
- **Include tests** - Show how to test the pattern
- **Full file paths** - With line numbers
## What NOT to Do
- Don't show broken or deprecated patterns
- Don't include overly complex examples
- Don't miss the test examples
- Don't show patterns without context
- Don't recommend without evidence
Remember: You're providing templates and examples developers can adapt. Show them how it's been done successfully before.

View File

@@ -1,40 +0,0 @@
# Commit Changes
You are tasked with creating git commits for the changes made during this session.
## Process:
1. **Think about what changed:**
- Review the conversation history and understand what was accomplished
- Run `git status` to see current changes
- Run `git diff` to understand the modifications
- Consider whether changes should be one commit or multiple logical commits
2. **Plan your commit(s):**
- Identify which files belong together
- Draft clear, descriptive commit messages
- Use imperative mood in commit messages
- Focus on why the changes were made, not just what
3. **Present your plan to the user:**
- List the files you plan to add for each commit
- Show the commit message(s) you'll use
- Ask: "I plan to create [N] commit(s) with these changes. Shall I proceed?"
4. **Execute upon confirmation:**
- Use `git add` with specific files (never use `-A` or `.`)
- Create commits with your planned messages
- Show the result with `git log --oneline -n [number]`
## Important:
- **NEVER add co-author information or Claude attribution**
- Commits should be authored solely by the user
- Do not include any "Generated with Claude" messages
- Do not add "Co-Authored-By" lines
- Write commit messages as if the user wrote them
## Remember:
- You have the full context of what was done in this session
- Group related changes together
- Keep commits focused and atomic when possible
- The user trusts your judgment - they asked you to commit

View File

@@ -1,448 +0,0 @@
# Implementation Plan
You are tasked with creating detailed implementation plans through an interactive, iterative process. You should be skeptical, thorough, and work collaboratively with the user to produce high-quality technical specifications.
## Initial Response
When this command is invoked:
1. **Check if parameters were provided**:
- If a file path or ticket reference was provided as a parameter, skip the default message
- Immediately read any provided files FULLY
- Begin the research process
2. **If no parameters provided**, respond with:
```
I'll help you create a detailed implementation plan. Let me start by understanding what we're building.
Please provide:
1. The task/ticket description (or reference to a ticket file)
2. Any relevant context, constraints, or specific requirements
3. Links to related research or previous implementations
I'll analyze this information and work with you to create a comprehensive plan.
Tip: You can also invoke this command with a ticket file directly: `/create_plan thoughts/allison/tickets/eng_1234.md`
For deeper analysis, try: `/create_plan think deeply about thoughts/allison/tickets/eng_1234.md`
```
Then wait for the user's input.
## Process Steps
### Step 1: Context Gathering & Initial Analysis
1. **Read all mentioned files immediately and FULLY**:
- Ticket files (e.g., `thoughts/allison/tickets/eng_1234.md`)
- Research documents
- Related implementation plans
- Any JSON/data files mentioned
- **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files
- **CRITICAL**: DO NOT spawn sub-tasks before reading these files yourself in the main context
- **NEVER** read files partially - if a file is mentioned, read it completely
2. **Spawn initial research tasks to gather context**:
Before asking the user any questions, use specialized agents to research in parallel:
- Use the **codebase-locator** agent to find all files related to the ticket/task
- Use the **codebase-analyzer** agent to understand how the current implementation works
These agents will:
- Find relevant source files, configs, and tests
- Identify the specific directories to focus on (e.g., if WUI is mentioned, they'll focus on humanlayer-wui/)
- Trace data flow and key functions
- Return detailed explanations with file:line references
3. **Read all files identified by research tasks**:
- After research tasks complete, read ALL files they identified as relevant
- Read them FULLY into the main context
- This ensures you have complete understanding before proceeding
4. **Analyze and verify understanding**:
- Cross-reference the ticket requirements with actual code
- Identify any discrepancies or misunderstandings
- Note assumptions that need verification
- Determine true scope based on codebase reality
5. **Present informed understanding and focused questions**:
```
Based on the ticket and my research of the codebase, I understand we need to [accurate summary].
I've found that:
- [Current implementation detail with file:line reference]
- [Relevant pattern or constraint discovered]
- [Potential complexity or edge case identified]
Questions that my research couldn't answer:
- [Specific technical question that requires human judgment]
- [Business logic clarification]
- [Design preference that affects implementation]
```
Only ask questions that you genuinely cannot answer through code investigation.
### Step 2: Research & Discovery
After getting initial clarifications:
1. **If the user corrects any misunderstanding**:
- DO NOT just accept the correction
- Spawn new research tasks to verify the correct information
- Read the specific files/directories they mention
- Only proceed once you've verified the facts yourself
2. **Create a research todo list** using TodoWrite to track exploration tasks
3. **Spawn parallel sub-tasks for comprehensive research**:
- Create multiple Task agents to research different aspects concurrently
- Use the right agent for each type of research:
**For deeper investigation:**
- **codebase-locator** - To find more specific files (e.g., "find all files that handle [specific component]")
- **codebase-analyzer** - To understand implementation details (e.g., "analyze how [system] works")
- **codebase-pattern-finder** - To find similar features we can model after
**For historical context:**
- **thoughts-locator** - To find any research, plans, or decisions about this area
- **thoughts-analyzer** - To extract key insights from the most relevant documents
**For related tickets:**
- **linear-searcher** - To find similar issues or past implementations
Each agent knows how to:
- Find the right files and code patterns
- Identify conventions and patterns to follow
- Look for integration points and dependencies
- Return specific file:line references
- Find tests and examples
4. **Wait for ALL sub-tasks to complete** before proceeding
5. **Present findings and design options**:
```
Based on my research, here's what I found:
**Current State:**
- [Key discovery about existing code]
- [Pattern or convention to follow]
**Design Options:**
1. [Option A] - [pros/cons]
2. [Option B] - [pros/cons]
**Open Questions:**
- [Technical uncertainty]
- [Design decision needed]
Which approach aligns best with your vision?
```
### Step 3: Plan Structure Development
Once aligned on approach:
1. **Create initial plan outline**:
```
Here's my proposed plan structure:
## Overview
[1-2 sentence summary]
## Implementation Phases:
1. [Phase name] - [what it accomplishes]
2. [Phase name] - [what it accomplishes]
3. [Phase name] - [what it accomplishes]
Does this phasing make sense? Should I adjust the order or granularity?
```
2. **Get feedback on structure** before writing details
### Step 4: Detailed Plan Writing
After structure approval:
1. **Write the plan** to `thoughts/shared/plans/{descriptive_name}.md`
2. **Use this template structure**:
````markdown
# [Feature/Task Name] Implementation Plan
## Overview
[Brief description of what we're implementing and why]
## Current State Analysis
[What exists now, what's missing, key constraints discovered]
## Desired End State
[A Specification of the desired end state after this plan is complete, and how to verify it]
### Key Discoveries:
- [Important finding with file:line reference]
- [Pattern to follow]
- [Constraint to work within]
## What We're NOT Doing
[Explicitly list out-of-scope items to prevent scope creep]
## Implementation Approach
[High-level strategy and reasoning]
## Phase 1: [Descriptive Name]
### Overview
[What this phase accomplishes]
### Changes Required:
#### 1. [Component/File Group]
**File**: `path/to/file.ext`
**Changes**: [Summary of changes]
```[language]
// Specific code to add/modify
```
````
### Success Criteria:
#### Automated Verification:
- [ ] Migration applies cleanly: `make migrate`
- [ ] Unit tests pass: `make test-component`
- [ ] Type checking passes: `npm run typecheck`
- [ ] Linting passes: `make lint`
- [ ] Integration tests pass: `make test-integration`
#### Manual Verification:
- [ ] Feature works as expected when tested via UI
- [ ] Performance is acceptable under load
- [ ] Edge case handling verified manually
- [ ] No regressions in related features
---
## Phase 2: [Descriptive Name]
[Similar structure with both automated and manual success criteria...]
---
## Testing Strategy
### Unit Tests:
- [What to test]
- [Key edge cases]
### Integration Tests:
- [End-to-end scenarios]
### Manual Testing Steps:
1. [Specific step to verify feature]
2. [Another verification step]
3. [Edge case to test manually]
## Performance Considerations
[Any performance implications or optimizations needed]
## Migration Notes
[If applicable, how to handle existing data/systems]
## References
- Original ticket: `thoughts/allison/tickets/eng_XXXX.md`
- Related research: `thoughts/shared/research/[relevant].md`
- Similar implementation: `[file:line]`
```
### Step 5: Sync and Review
1. **Sync the thoughts directory**:
- Run `humanlayer thoughts sync` to sync the newly created plan
- This ensures the plan is properly indexed and available
2. **Present the draft plan location**:
```
I've created the initial implementation plan at:
`thoughts/shared/plans/[filename].md`
Please review it and let me know:
- Are the phases properly scoped?
- Are the success criteria specific enough?
- Any technical details that need adjustment?
- Missing edge cases or considerations?
````
3. **Iterate based on feedback** - be ready to:
- Add missing phases
- Adjust technical approach
- Clarify success criteria (both automated and manual)
- Add/remove scope items
- After making changes, run `humanlayer thoughts sync` again
4. **Continue refining** until the user is satisfied
## Important Guidelines
1. **Be Skeptical**:
- Question vague requirements
- Identify potential issues early
- Ask "why" and "what about"
- Don't assume - verify with code
2. **Be Interactive**:
- Don't write the full plan in one shot
- Get buy-in at each major step
- Allow course corrections
- Work collaboratively
3. **Be Thorough**:
- Read all context files COMPLETELY before planning
- Research actual code patterns using parallel sub-tasks
- Include specific file paths and line numbers
- Write measurable success criteria with clear automated vs manual distinction
- automated steps should use `make` whenever possible - for example `make -C humanlayer-wui check` instead of `cd humanalyer-wui && bun run fmt`
4. **Be Practical**:
- Focus on incremental, testable changes
- Consider migration and rollback
- Think about edge cases
- Include "what we're NOT doing"
5. **Track Progress**:
- Use TodoWrite to track planning tasks
- Update todos as you complete research
- Mark planning tasks complete when done
6. **No Open Questions in Final Plan**:
- If you encounter open questions during planning, STOP
- Research or ask for clarification immediately
- Do NOT write the plan with unresolved questions
- The implementation plan must be complete and actionable
- Every decision must be made before finalizing the plan
## Success Criteria Guidelines
**Always separate success criteria into two categories:**
1. **Automated Verification** (can be run by execution agents):
- Commands that can be run: `make test`, `npm run lint`, etc.
- Specific files that should exist
- Code compilation/type checking
- Automated test suites
2. **Manual Verification** (requires human testing):
- UI/UX functionality
- Performance under real conditions
- Edge cases that are hard to automate
- User acceptance criteria
**Format example:**
```markdown
### Success Criteria:
#### Automated Verification:
- [ ] Database migration runs successfully: `make migrate`
- [ ] All unit tests pass: `go test ./...`
- [ ] No linting errors: `golangci-lint run`
- [ ] API endpoint returns 200: `curl localhost:8080/api/new-endpoint`
#### Manual Verification:
- [ ] New feature appears correctly in the UI
- [ ] Performance is acceptable with 1000+ items
- [ ] Error messages are user-friendly
- [ ] Feature works correctly on mobile devices
````
## Common Patterns
### For Database Changes:
- Start with schema/migration
- Add store methods
- Update business logic
- Expose via API
- Update clients
### For New Features:
- Research existing patterns first
- Start with data model
- Build backend logic
- Add API endpoints
- Implement UI last
### For Refactoring:
- Document current behavior
- Plan incremental changes
- Maintain backwards compatibility
- Include migration strategy
## Sub-task Spawning Best Practices
When spawning research sub-tasks:
1. **Spawn multiple tasks in parallel** for efficiency
2. **Each task should be focused** on a specific area
3. **Provide detailed instructions** including:
- Exactly what to search for
- Which directories to focus on
- What information to extract
- Expected output format
4. **Specify read-only tools** to use
5. **Request specific file:line references** in responses
6. **Wait for all tasks to complete** before synthesizing
7. **Verify sub-task results**:
- If a sub-task returns unexpected results, spawn follow-up tasks
- Cross-check findings against the actual codebase
- Don't accept results that seem incorrect
Example of spawning multiple tasks:
```python
# Spawn these tasks concurrently:
tasks = [
Task("Research database schema", db_research_prompt),
Task("Find API patterns", api_research_prompt),
Task("Investigate UI components", ui_research_prompt),
Task("Check test patterns", test_research_prompt)
]
```
## Example Interaction Flow
```
User: /implementation_plan
Assistant: I'll help you create a detailed implementation plan...
User: We need to add parent-child tracking for Claude sub-tasks. See thoughts/allison/tickets/eng_1478.md
Assistant: Let me read that ticket file completely first...
[Reads file fully]
Based on the ticket, I understand we need to track parent-child relationships for Claude sub-task events in the hld daemon. Before I start planning, I have some questions...
[Interactive process continues...]
```

View File

@@ -1,37 +0,0 @@
2. set up worktree for implementation:
2a. read `hack/create_worktree.sh` and create a new worktree with the Linear branch name: `./hack/create_worktree.sh ENG-XXXX BRANCH_NAME`
3. determine required data:
branch name
path to plan file (use relative path only)
launch prompt
command to run
**IMPORTANT PATH USAGE:**
- The thoughts/ directory is synced between the main repo and worktrees
- Always use ONLY the relative path starting with `thoughts/shared/...` without any directory prefix
- Example: `thoughts/shared/plans/fix-mcp-keepalive-proper.md` (not the full absolute path)
- This works because thoughts are synced and accessible from the worktree
3a. confirm with the user by sending a message to the Human
```
based on the input, I plan to create a worktree with the following details:
worktree path: ~/wt/humanlayer/ENG-XXXX
branch name: BRANCH_NAME
path to plan file: $FILEPATH
launch prompt:
/implement_plan at $FILEPATH and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link
command to run:
humanlayer launch --model opus -w ~/wt/humanlayer/ENG-XXXX "/implement_plan at $FILEPATH and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link"
```
incorporate any user feedback then:
4. launch implementation session: `humanlayer launch --model opus -w ~/wt/humanlayer/ENG-XXXX "/implement_plan at $FILEPATH and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link"`

View File

@@ -1,196 +0,0 @@
# Debug
You are tasked with helping debug issues during manual testing or implementation. This command allows you to investigate problems by examining logs, database state, and git history without editing files. Think of this as a way to bootstrap a debugging session without using the primary window's context.
## Initial Response
When invoked WITH a plan/ticket file:
```
I'll help debug issues with [file name]. Let me understand the current state.
What specific problem are you encountering?
- What were you trying to test/implement?
- What went wrong?
- Any error messages?
I'll investigate the logs, database, and git state to help figure out what's happening.
```
When invoked WITHOUT parameters:
```
I'll help debug your current issue.
Please describe what's going wrong:
- What are you working on?
- What specific problem occurred?
- When did it last work?
I can investigate logs, database state, and recent changes to help identify the issue.
```
## Environment Information
You have access to these key locations and tools:
**Logs** (automatically created by `make daemon` and `make wui`):
- MCP logs: `~/.humanlayer/logs/mcp-claude-approvals-*.log`
- Combined WUI/Daemon logs: `~/.humanlayer/logs/wui-${BRANCH_NAME}/codelayer.log`
- First line shows: `[timestamp] starting [service] in [directory]`
**Database**:
- Location: `~/.humanlayer/daemon-{BRANCH_NAME}.db`
- SQLite database with sessions, events, approvals, etc.
- Can query directly with `sqlite3`
**Git State**:
- Check current branch, recent commits, uncommitted changes
- Similar to how `commit` and `describe_pr` commands work
**Service Status**:
- Check if daemon is running: `ps aux | grep hld`
- Check if WUI is running: `ps aux | grep wui`
- Socket exists: `~/.humanlayer/daemon.sock`
## Process Steps
### Step 1: Understand the Problem
After the user describes the issue:
1. **Read any provided context** (plan or ticket file):
- Understand what they're implementing/testing
- Note which phase or step they're on
- Identify expected vs actual behavior
2. **Quick state check**:
- Current git branch and recent commits
- Any uncommitted changes
- When the issue started occurring
### Step 2: Investigate the Issue
Spawn parallel Task agents for efficient investigation:
```
Task 1 - Check Recent Logs:
Find and analyze the most recent logs for errors:
1. Find latest daemon log: ls -t ~/.humanlayer/logs/daemon-*.log | head -1
2. Find latest WUI log: ls -t ~/.humanlayer/logs/wui-*.log | head -1
3. Search for errors, warnings, or issues around the problem timeframe
4. Note the working directory (first line of log)
5. Look for stack traces or repeated errors
Return: Key errors/warnings with timestamps
```
```
Task 2 - Database State:
Check the current database state:
1. Connect to database: sqlite3 ~/.humanlayer/daemon.db
2. Check schema: .tables and .schema for relevant tables
3. Query recent data:
- SELECT * FROM sessions ORDER BY created_at DESC LIMIT 5;
- SELECT * FROM conversation_events WHERE created_at > datetime('now', '-1 hour');
- Other queries based on the issue
4. Look for stuck states or anomalies
Return: Relevant database findings
```
```
Task 3 - Git and File State:
Understand what changed recently:
1. Check git status and current branch
2. Look at recent commits: git log --oneline -10
3. Check uncommitted changes: git diff
4. Verify expected files exist
5. Look for any file permission issues
Return: Git state and any file issues
```
### Step 3: Present Findings
Based on the investigation, present a focused debug report:
```markdown
## Debug Report
### What's Wrong
[Clear statement of the issue based on evidence]
### Evidence Found
**From Logs** (`~/.humanlayer/logs/`):
- [Error/warning with timestamp]
- [Pattern or repeated issue]
**From Database**:
```sql
-- Relevant query and result
[Finding from database]
```
**From Git/Files**:
- [Recent changes that might be related]
- [File state issues]
### Root Cause
[Most likely explanation based on evidence]
### Next Steps
1. **Try This First**:
```bash
[Specific command or action]
```
2. **If That Doesn't Work**:
- Restart services: `make daemon` and `make wui`
- Check browser console for WUI errors
- Run with debug: `HUMANLAYER_DEBUG=true make daemon`
### Can't Access?
Some issues might be outside my reach:
- Browser console errors (F12 in browser)
- MCP server internal state
- System-level issues
Would you like me to investigate something specific further?
```
## Important Notes
- **Focus on manual testing scenarios** - This is for debugging during implementation
- **Always require problem description** - Can't debug without knowing what's wrong
- **Read files completely** - No limit/offset when reading context
- **Think like `commit` or `describe_pr`** - Understand git state and changes
- **Guide back to user** - Some issues (browser console, MCP internals) are outside reach
- **No file editing** - Pure investigation only
## Quick Reference
**Find Latest Logs**:
```bash
ls -t ~/.humanlayer/logs/daemon-*.log | head -1
ls -t ~/.humanlayer/logs/wui-*.log | head -1
```
**Database Queries**:
```bash
sqlite3 ~/.humanlayer/daemon.db ".tables"
sqlite3 ~/.humanlayer/daemon.db ".schema sessions"
sqlite3 ~/.humanlayer/daemon.db "SELECT * FROM sessions ORDER BY created_at DESC LIMIT 5;"
```
**Service Check**:
```bash
ps aux | grep hld # Is daemon running?
ps aux | grep wui # Is WUI running?
```
**Git State**:
```bash
git status
git log --oneline -10
git diff
```
Remember: This command helps you investigate without burning the primary window's context. Perfect for when you hit an issue during manual testing and need to dig into logs, database, or git state.

View File

@@ -1,71 +0,0 @@
# Generate PR Description
You are tasked with generating a comprehensive pull request description following the repository's standard template.
## Steps to follow:
1. **Read the PR description template:**
- First, check if `thoughts/shared/pr_description.md` exists
- If it doesn't exist, inform the user that their `humanlayer thoughts` setup is incomplete and they need to create a PR description template at `thoughts/shared/pr_description.md`
- Read the template carefully to understand all sections and requirements
2. **Identify the PR to describe:**
- Check if the current branch has an associated PR: `gh pr view --json url,number,title,state 2>/dev/null`
- If no PR exists for the current branch, or if on main/master, list open PRs: `gh pr list --limit 10 --json number,title,headRefName,author`
- Ask the user which PR they want to describe
3. **Check for existing description:**
- Check if `thoughts/shared/prs/{number}_description.md` already exists
- If it exists, read it and inform the user you'll be updating it
- Consider what has changed since the last description was written
4. **Gather comprehensive PR information:**
- Get the full PR diff: `gh pr diff {number}`
- If you get an error about no default remote repository, instruct the user to run `gh repo set-default` and select the appropriate repository
- Get commit history: `gh pr view {number} --json commits`
- Review the base branch: `gh pr view {number} --json baseRefName`
- Get PR metadata: `gh pr view {number} --json url,title,number,state`
5. **Analyze the changes thoroughly:** (ultrathink about the code changes, their architectural implications, and potential impacts)
- Read through the entire diff carefully
- For context, read any files that are referenced but not shown in the diff
- Understand the purpose and impact of each change
- Identify user-facing changes vs internal implementation details
- Look for breaking changes or migration requirements
6. **Handle verification requirements:**
- Look for any checklist items in the "How to verify it" section of the template
- For each verification step:
- If it's a command you can run (like `make check test`, `npm test`, etc.), run it
- If it passes, mark the checkbox as checked: `- [x]`
- If it fails, keep it unchecked and note what failed: `- [ ]` with explanation
- If it requires manual testing (UI interactions, external services), leave unchecked and note for user
- Document any verification steps you couldn't complete
7. **Generate the description:**
- Fill out each section from the template thoroughly:
- Answer each question/section based on your analysis
- Be specific about problems solved and changes made
- Focus on user impact where relevant
- Include technical details in appropriate sections
- Write a concise changelog entry
- Ensure all checklist items are addressed (checked or explained)
8. **Save and sync the description:**
- Write the completed description to `thoughts/shared/prs/{number}_description.md`
- Run `humanlayer thoughts sync` to sync the thoughts directory
- Show the user the generated description
9. **Update the PR:**
- Update the PR description directly: `gh pr edit {number} --body-file thoughts/shared/prs/{number}_description.md`
- Confirm the update was successful
- If any verification steps remain unchecked, remind the user to complete them before merging
## Important notes:
- This command works across different repositories - always read the local template
- Be thorough but concise - descriptions should be scannable
- Focus on the "why" as much as the "what"
- Include any breaking changes or migration notes prominently
- If the PR touches multiple components, organize the description accordingly
- Always attempt to run verification commands when possible
- Clearly communicate which verification steps need manual testing

View File

@@ -1,65 +0,0 @@
# Implement Plan
You are tasked with implementing an approved technical plan from `thoughts/shared/plans/`. These plans contain phases with specific changes and success criteria.
## Getting Started
When given a plan path:
- Read the plan completely and check for any existing checkmarks (- [x])
- Read the original ticket and all files mentioned in the plan
- **Read files fully** - never use limit/offset parameters, you need complete context
- Think deeply about how the pieces fit together
- Create a todo list to track your progress
- Start implementing if you understand what needs to be done
If no plan path provided, ask for one.
## Implementation Philosophy
Plans are carefully designed, but reality can be messy. Your job is to:
- Follow the plan's intent while adapting to what you find
- Implement each phase fully before moving to the next
- Verify your work makes sense in the broader codebase context
- Update checkboxes in the plan as you complete sections
When things don't match the plan exactly, think about why and communicate clearly. The plan is your guide, but your judgment matters too.
If you encounter a mismatch:
- STOP and think deeply about why the plan can't be followed
- Present the issue clearly:
```
Issue in Phase [N]:
Expected: [what the plan says]
Found: [actual situation]
Why this matters: [explanation]
How should I proceed?
```
## Verification Approach
After implementing a phase:
- Run the success criteria checks (usually `cargo test -p [crate_name]` covers everything)
- Fix any issues before proceeding
- Update your progress in both the plan and your todos
- Check off completed items in the plan file itself using Edit
Don't let verification interrupt your flow - batch it at natural stopping points.
## If You Get Stuck
When something isn't working as expected:
- First, make sure you've read and understood all the relevant code
- Consider if the codebase has evolved since the plan was written
- Present the mismatch clearly and ask for guidance
Use sub-tasks sparingly - mainly for targeted debugging or exploring unfamiliar territory.
## Resuming Work
If the plan has existing checkmarks:
- Trust that completed work is done
- Pick up from the first unchecked item
- Verify previous work only if something seems off
Remember: You're implementing a solution, not just checking boxes. Keep the end goal in mind and maintain forward momentum.

View File

@@ -1,44 +0,0 @@
# Local Review
You are tasked with setting up a local review environment for a colleague's branch. This involves creating a worktree, setting up dependencies, and launching a new Claude Code session.
## Process
When invoked with a parameter like `gh_username:branchName`:
1. **Parse the input**:
- Extract GitHub username and branch name from the format `username:branchname`
- If no parameter provided, ask for it in the format: `gh_username:branchName`
2. **Extract ticket information**:
- Look for ticket numbers in the branch name (e.g., `eng-1696`, `ENG-1696`)
- Use this to create a short worktree directory name
- If no ticket found, use a sanitized version of the branch name
3. **Set up the remote and worktree**:
- Check if the remote already exists using `git remote -v`
- If not, add it: `git remote add USERNAME git@github.com:USERNAME/humanlayer`
- Fetch from the remote: `git fetch USERNAME`
- Create worktree: `git worktree add -b BRANCHNAME ~/wt/humanlayer/SHORT_NAME USERNAME/BRANCHNAME`
4. **Configure the worktree**:
- Copy Claude settings: `cp .claude/settings.local.json WORKTREE/.claude/`
- Run setup: `make -C WORKTREE setup`
- Initialize thoughts: `cd WORKTREE && npx humanlayer thoughts init --directory humanlayer`
## Error Handling
- If worktree already exists, inform the user they need to remove it first
- If remote fetch fails, check if the username/repo exists
- If setup fails, provide the error but continue with the launch
## Example Usage
```
/local_review samdickson22:sam/eng-1696-hotkey-for-yolo-mode
```
This will:
- Add 'samdickson22' as a remote
- Create worktree at `~/wt/humanlayer/eng-1696`
- Set up the environment

View File

@@ -1,28 +0,0 @@
## PART I - IF A TICKET IS MENTIONED
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand the implementation plan and any concerns
## PART I - IF NO TICKET IS MENTIOND
0. read .claude/commands/linear.md
0a. fetch the top 10 priority items from linear in status "ready for dev" using the MCP tools, noting all items in the `links` section
0b. select the highest priority SMALL or XS issue from the list (if no SMALL or XS issues exist, EXIT IMMEDIATELY and inform the user)
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand the implementation plan and any concerns
## PART II - NEXT STEPS
think deeply
1. move the item to "in dev" using the MCP tools
1a. identify the linked implementation plan document from the `links` section
1b. if no plan exists, move the ticket back to "ready for spec" and EXIT with an explanation
think deeply about the implementation
2. set up worktree for implementation:
2a. read `hack/create_worktree.sh` and create a new worktree with the Linear branch name: `./hack/create_worktree.sh ENG-XXXX BRANCH_NAME`
2b. launch implementation session: `npx humanlayer launch --model opus -w ~/wt/humanlayer/ENG-XXXX "/implement_plan and when you are done implementing and all tests pass, read ./claude/commands/commit.md and create a commit, then read ./claude/commands/describe_pr.md and create a PR, then add a comment to the Linear ticket with the PR link"`
think deeply, use TodoWrite to track your tasks. When fetching from linear, get the top 10 items by priority but only work on ONE item - specifically the highest priority SMALL or XS sized issue.

View File

@@ -1,30 +0,0 @@
## PART I - IF A TICKET IS MENTIONED
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to learn about past implementations and research, and any questions or concerns about them
### PART I - IF NO TICKET IS MENTIONED
0. read .claude/commands/linear.md
0a. fetch the top 10 priority items from linear in status "ready for spec" using the MCP tools, noting all items in the `links` section
0b. select the highest priority SMALL or XS issue from the list (if no SMALL or XS issues exist, EXIT IMMEDIATELY and inform the user)
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to learn about past implementations and research, and any questions or concerns about them
### PART II - NEXT STEPS
think deeply
1. move the item to "plan in progress" using the MCP tools
1a. read ./claude/commands/create_plan.md
1b. determine if the item has a linked implementation plan document based on the `links` section
1d. if the plan exists, you're done, respond with a link to the ticket
1e. if the research is insufficient or has unaswered questions, create a new plan document following the instructions in ./claude/commands/create_plan.md
think deeply
2. when the plan is complete, `humanlayer thoughts sync` and attach the doc to the ticket using the MCP tools and create a terse comment with a link to it (re-read .claude/commands/linear.md if needed)
2a. move the item to "plan in review" using the MCP tools
think deeply, use TodoWrite to track your tasks. When fetching from linear, get the top 10 items by priority but only work on ONE item - specifically the highest priority SMALL or XS sized issue.

View File

@@ -1,46 +0,0 @@
## PART I - IF A LINEAR TICKET IS MENTIONED
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand what research is needed and any previous attempts
## PART I - IF NO TICKET IS MENTIONED
0. read .claude/commands/linear.md
0a. fetch the top 10 priority items from linear in status "research needed" using the MCP tools, noting all items in the `links` section
0b. select the highest priority SMALL or XS issue from the list (if no SMALL or XS issues exist, EXIT IMMEDIATELY and inform the user)
0c. use `linear` cli to fetch the selected item into thoughts with the ticket number - ./thoughts/shared/tickets/ENG-xxxx.md
0d. read the ticket and all comments to understand what research is needed and any previous attempts
## PART II - NEXT STEPS
think deeply
1. move the item to "research in progress" using the MCP tools
1a. read any linked documents in the `links` section to understand context
1b. if insufficient information to conduct research, add a comment asking for clarification and move back to "research needed"
think deeply about the research needs
2. conduct the research:
2a. read .claude/commands/research_codebase.md for guidance on effective codebase research
2b. if the linear comments suggest web research is needed, use WebSearch to research external solutions, APIs, or best practices
2c. search the codebase for relevant implementations and patterns
2d. examine existing similar features or related code
2e. identify technical constraints and opportunities
2f. Be unbiased - don't think too much about an ideal implementation plan, just document all related files and how the systems work today
2g. document findings in a new thoughts document: `thoughts/shared/research/ENG-XXXX_research.md`
think deeply about the findings
3. synthesize research into actionable insights:
3a. summarize key findings and technical decisions
3b. identify potential implementation approaches
3c. note any risks or concerns discovered
3d. run `humanlayer thoughts sync` to save the research
4. update the ticket:
4a. attach the research document to the ticket using the MCP tools with proper link formatting
4b. add a comment summarizing the research outcomes
4c. move the item to "research in review" using the MCP tools
think deeply, use TodoWrite to track your tasks. When fetching from linear, get the top 10 items by priority but only work on ONE item - specifically the highest priority issue.

View File

@@ -1,172 +0,0 @@
# Research Codebase
You are tasked with conducting comprehensive research across the codebase to answer user questions by spawning parallel sub-agents and synthesizing their findings.
## Initial Setup:
When this command is invoked, respond with:
```
I'm ready to research the codebase. Please provide your research question or area of interest, and I'll analyze it thoroughly by exploring relevant components and connections.
```
Then wait for the user's research query.
## Steps to follow after receiving the research query:
1. **Read any directly mentioned files first:**
- If the user mentions specific files (crates, docs, JSON), read them FULLY first
- **IMPORTANT**: Use the Read tool WITHOUT limit/offset parameters to read entire files
- **CRITICAL**: Read these files yourself in the main context before spawning any sub-tasks
- This ensures you have full context before decomposing the research
2. **Analyze and decompose the research question:**
- Break down the user's query into composable research areas
- Take time to ultrathink about the underlying patterns, connections, and architectural implications the user might be seeking
- Identify specific components, patterns, or concepts to investigate
- Create a research plan using TodoWrite to track all subtasks
- Consider which directories, files, or architectural patterns are relevant
3. **Spawn parallel sub-agent tasks for comprehensive research:**
- Create multiple Task agents to research different aspects concurrently
- We now have specialized agents that know how to do specific research tasks:
**For codebase research:**
- Use the **codebase-locator** agent to find WHERE files and components live
- Use the **codebase-analyzer** agent to understand HOW specific code works
The key is to use these agents intelligently:
- Start with locator agents to find what exists
- Then use analyzer agents on the most promising findings
- Run multiple agents in parallel when they're searching for different things
- Each agent knows its job - just tell it what you're looking for
- Don't write detailed prompts about HOW to search - the agents already know
4. **Wait for all sub-agents to complete and synthesize findings:**
- IMPORTANT: Wait for ALL sub-agent tasks to complete before proceeding
- Compile all sub-agent results (both codebase and thoughts findings)
- Prioritize live codebase findings as primary source of truth
- Use thoughts/ findings as supplementary historical context
- Connect findings across different components
- Include specific file paths and line numbers for reference
- Verify all thoughts/ paths are correct (e.g., thoughts/allison/ not thoughts/shared/ for personal files)
- Highlight patterns, connections, and architectural decisions
- Answer the user's specific questions with concrete evidence
5. **Gather metadata for the research document:**
- Run the `zed/script/spec_metadata.sh` script to generate all relevant metadata
- Filename: `thoughts/shared/research/YYYY-MM-DD_HH-MM-SS_topic.md`
6. **Generate research document:**
- Use the metadata gathered in step 4
- Structure the document with YAML frontmatter followed by content:
```markdown
---
date: [Current date and time with timezone in ISO format]
researcher: [Researcher name from thoughts status]
git_commit: [Current commit hash]
branch: [Current branch name]
repository: [Repository name]
topic: "[User's Question/Topic]"
tags: [research, codebase, relevant-component-names]
status: complete
last_updated: [Current date in YYYY-MM-DD format]
last_updated_by: [Researcher name]
---
# Research: [User's Question/Topic]
**Date**: [Current date and time with timezone from step 4]
**Researcher**: [Researcher name from thoughts status]
**Git Commit**: [Current commit hash from step 4]
**Branch**: [Current branch name from step 4]
**Repository**: [Repository name]
## Research Question
[Original user query]
## Summary
[High-level findings answering the user's question]
## Detailed Findings
### [Component/Area 1]
- Finding with reference ([file.ext:line](link))
- Connection to other components
- Implementation details
### [Component/Area 2]
...
## Code References
- `path/to/file.py:123` - Description of what's there
- `another/file.ts:45-67` - Description of the code block
## Architecture Insights
[Patterns, conventions, and design decisions discovered]
## Historical Context (from thoughts/)
[Relevant insights from thoughts/ directory with references]
- `thoughts/shared/something.md` - Historical decision about X
- `thoughts/local/notes.md` - Past exploration of Y
Note: Paths exclude "searchable/" even if found there
## Related Research
[Links to other research documents in thoughts/shared/research/]
## Open Questions
[Any areas that need further investigation]
```
7. **Add GitHub permalinks (if applicable):**
- Check if on main branch or if commit is pushed: `git branch --show-current` and `git status`
- If on main/master or pushed, generate GitHub permalinks:
- Get repo info: `gh repo view --json owner,name`
- Create permalinks: `https://github.com/{owner}/{repo}/blob/{commit}/{file}#L{line}`
- Replace local file references with permalinks in the document
8. **Handle follow-up questions:**
- If the user has follow-up questions, append to the same research document
- Update the frontmatter fields `last_updated` and `last_updated_by` to reflect the update
- Add `last_updated_note: "Added follow-up research for [brief description]"` to frontmatter
- Add a new section: `## Follow-up Research [timestamp]`
- Spawn new sub-agents as needed for additional investigation
- Continue updating the document and syncing
## Important notes:
- Always use parallel Task agents to maximize efficiency and minimize context usage
- Always run fresh codebase research - never rely solely on existing research documents
- The thoughts/ directory provides historical context to supplement live findings
- Focus on finding concrete file paths and line numbers for developer reference
- Research documents should be self-contained with all necessary context
- Each sub-agent prompt should be specific and focused on read-only operations
- Consider cross-component connections and architectural patterns
- Include temporal context (when the research was conducted)
- Link to GitHub when possible for permanent references
- Keep the main agent focused on synthesis, not deep file reading
- Encourage sub-agents to find examples and usage patterns, not just definitions
- Explore all of thoughts/ directory, not just research subdirectory
- **File reading**: Always read mentioned files FULLY (no limit/offset) before spawning sub-tasks
- **Critical ordering**: Follow the numbered steps exactly
- ALWAYS read mentioned files first before spawning sub-tasks (step 1)
- ALWAYS wait for all sub-agents to complete before synthesizing (step 4)
- ALWAYS gather metadata before writing the document (step 5 before step 6)
- NEVER write the research document with placeholder values
- **Frontmatter consistency**:
- Always include frontmatter at the beginning of research documents
- Keep frontmatter fields consistent across all research documents
- Update frontmatter when adding follow-up research
- Use snake_case for multi-word field names (e.g., `last_updated`, `git_commit`)
- Tags should be relevant to the research topic and components studied

View File

@@ -1,162 +0,0 @@
# Validate Plan
You are tasked with validating that an implementation plan was correctly executed, verifying all success criteria and identifying any deviations or issues.
## Initial Setup
When invoked:
1. **Determine context** - Are you in an existing conversation or starting fresh?
- If existing: Review what was implemented in this session
- If fresh: Need to discover what was done through git and codebase analysis
2. **Locate the plan**:
- If plan path provided, use it
- Otherwise, search recent commits for plan references or ask user
3. **Gather implementation evidence**:
```bash
# Check recent commits
git log --oneline -n 20
git diff HEAD~N..HEAD # Where N covers implementation commits
# Run comprehensive checks
cd $(git rev-parse --show-toplevel) && make check test
```
## Validation Process
### Step 1: Context Discovery
If starting fresh or need more context:
1. **Read the implementation plan** completely
2. **Identify what should have changed**:
- List all files that should be modified
- Note all success criteria (automated and manual)
- Identify key functionality to verify
3. **Spawn parallel research tasks** to discover implementation:
```
Task 1 - Verify database changes:
Research if migration [N] was added and schema changes match plan.
Check: migration files, schema version, table structure
Return: What was implemented vs what plan specified
Task 2 - Verify code changes:
Find all modified files related to [feature].
Compare actual changes to plan specifications.
Return: File-by-file comparison of planned vs actual
Task 3 - Verify test coverage:
Check if tests were added/modified as specified.
Run test commands and capture results.
Return: Test status and any missing coverage
```
### Step 2: Systematic Validation
For each phase in the plan:
1. **Check completion status**:
- Look for checkmarks in the plan (- [x])
- Verify the actual code matches claimed completion
2. **Run automated verification**:
- Execute each command from "Automated Verification"
- Document pass/fail status
- If failures, investigate root cause
3. **Assess manual criteria**:
- List what needs manual testing
- Provide clear steps for user verification
4. **Think deeply about edge cases**:
- Were error conditions handled?
- Are there missing validations?
- Could the implementation break existing functionality?
### Step 3: Generate Validation Report
Create comprehensive validation summary:
```markdown
## Validation Report: [Plan Name]
### Implementation Status
✓ Phase 1: [Name] - Fully implemented
✓ Phase 2: [Name] - Fully implemented
⚠️ Phase 3: [Name] - Partially implemented (see issues)
### Automated Verification Results
✓ Build passes: `make build`
✓ Tests pass: `make test`
✗ Linting issues: `make lint` (3 warnings)
### Code Review Findings
#### Matches Plan:
- Database migration correctly adds [table]
- API endpoints implement specified methods
- Error handling follows plan
#### Deviations from Plan:
- Used different variable names in [file:line]
- Added extra validation in [file:line] (improvement)
#### Potential Issues:
- Missing index on foreign key could impact performance
- No rollback handling in migration
### Manual Testing Required:
1. UI functionality:
- [ ] Verify [feature] appears correctly
- [ ] Test error states with invalid input
2. Integration:
- [ ] Confirm works with existing [component]
- [ ] Check performance with large datasets
### Recommendations:
- Address linting warnings before merge
- Consider adding integration test for [scenario]
- Document new API endpoints
```
## Working with Existing Context
If you were part of the implementation:
- Review the conversation history
- Check your todo list for what was completed
- Focus validation on work done in this session
- Be honest about any shortcuts or incomplete items
## Important Guidelines
1. **Be thorough but practical** - Focus on what matters
2. **Run all automated checks** - Don't skip verification commands
3. **Document everything** - Both successes and issues
4. **Think critically** - Question if the implementation truly solves the problem
5. **Consider maintenance** - Will this be maintainable long-term?
## Validation Checklist
Always verify:
- [ ] All phases marked complete are actually done
- [ ] Automated tests pass
- [ ] Code follows existing patterns
- [ ] No regressions introduced
- [ ] Error handling is robust
- [ ] Documentation updated if needed
- [ ] Manual test steps are clear
## Relationship to Other Commands
Recommended workflow:
1. `/implement_plan` - Execute the implementation
2. `/commit` - Create atomic commits for changes
3. `/validate_plan` - Verify implementation correctness
4. `/describe_pr` - Generate PR description
The validation works best after commits are made, as it can analyze the git history to understand what was implemented.
Remember: Good validation catches issues before they reach production. Be constructive but thorough in identifying gaps or improvements.

View File

@@ -1,10 +0,0 @@
{
"permissions": {
"allow": [
// "Bash(./hack/spec_metadata.sh)",
// "Bash(hack/spec_metadata.sh)",
// "Bash(bash hack/spec_metadata.sh)"
]
},
"enableAllProjectMcpServers": false
}

View File

@@ -1,31 +0,0 @@
{
"permissions": {
"allow": [
"Read(/Users/mikaylamaki/projects/zed-work/zed-monorepo-real/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Bash(git add:*)",
"Read(/Users/nathan/src/agent-client-protocol/rust/**)",
"Bash(./script/spec_metadata.sh:*)",
"Bash(npm run generate:*)",
"Bash(npm run typecheck:*)",
"Bash(npm run:*)",
"Bash(npm install)",
"Bash(grep:*)",
"Bash(find:*)",
"Bash(node:*)",
"Bash(cargo check:*)",
"Bash(cargo test)",
"Bash(npx tsc:*)"
],
"additionalDirectories": [
"/Users/mikaylamaki/projects/zed-work/zed-monorepo-real/claude-code-acp/",
"/Users/mikaylamaki/projects/zed-work/zed-monorepo-real/agentic-coding-protocol/",
"/Users/nathan/src/agent",
"/Users/nathan/src/agent-client-protocol/",
"/Users/nathan/src/claude-code-acp"
]
}
}

View File

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

3
.gitattributes vendored
View File

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

View File

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

View File

@@ -81,6 +81,7 @@ jobs:
echo "run_license=false" >> "$GITHUB_OUTPUT"
echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \
echo "$GITHUB_REF_NAME" | grep -qvP '^v[0-9]+\.[0-9]+\.[0-9x](-pre)?$' && \
echo "run_nix=true" >> "$GITHUB_OUTPUT" || \
echo "run_nix=false" >> "$GITHUB_OUTPUT"

View File

@@ -32,7 +32,7 @@ jobs:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@c840d45a03a323fbc3f7507ac7769dbd91bfb164 # v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
webhook-url: ${{ secrets.DISCORD_WEBHOOK_RELEASE_NOTES }}
content: ${{ steps.get-content.outputs.string }}
send_release_notes_email:

13
.rules
View File

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

View File

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

433
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -54,6 +54,8 @@ members = [
"crates/deepseek",
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/editor",
"crates/eval",
"crates/explorer_command_injector",
@@ -82,20 +84,21 @@ members = [
"crates/http_client_tls",
"crates/icons",
"crates/image_viewer",
"crates/edit_prediction",
"crates/edit_prediction_button",
"crates/inspector_ui",
"crates/install_cli",
"crates/jj",
"crates/jj_ui",
"crates/journal",
"crates/keymap_editor",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_models",
"crates/language_onboarding",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
@@ -130,6 +133,7 @@ members = [
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/release_channel",
"crates/scheduler",
"crates/remote",
"crates/remote_server",
"crates/repl",
@@ -140,12 +144,12 @@ members = [
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
"crates/session",
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/settings_ui_macros",
"crates/snippet",
"crates/snippet_provider",
"crates/snippets_ui",
@@ -156,9 +160,9 @@ members = [
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
"crates/system_specs",
"crates/supermaven_api",
"crates/svg_preview",
"crates/system_specs",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@@ -191,6 +195,7 @@ members = [
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
"crates/zed_env_vars",
"crates/zeta",
"crates/zeta_cli",
"crates/zlog",
@@ -207,7 +212,6 @@ members = [
"extensions/slash-commands-example",
"extensions/snippets",
"extensions/test-extension",
"extensions/toml",
#
# Tooling
@@ -297,9 +301,7 @@ git_hosting_providers = { path = "crates/git_hosting_providers" }
git_ui = { path = "crates/git_ui" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
] }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
@@ -314,13 +316,16 @@ install_cli = { path = "crates/install_cli" }
jj = { path = "crates/jj" }
jj_ui = { path = "crates/jj_ui" }
journal = { path = "crates/journal" }
keymap_editor = { path = "crates/keymap_editor" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_onboarding = { path = "crates/language_onboarding" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
line_ending_selector = { path = "crates/line_ending_selector" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
@@ -358,6 +363,7 @@ proto = { path = "crates/proto" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
scheduler = { path = "crates/scheduler" }
remote = { path = "crates/remote" }
remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
@@ -368,11 +374,11 @@ rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
settings_ui = { path = "crates/settings_ui" }
settings_ui_macros = { path = "crates/settings_ui_macros" }
snippet = { path = "crates/snippet" }
snippet_provider = { path = "crates/snippet_provider" }
snippets_ui = { path = "crates/snippets_ui" }
@@ -418,6 +424,7 @@ worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zed_env_vars = { path = "crates/zed_env_vars" }
zeta = { path = "crates/zeta" }
zlog = { path = "crates/zlog" }
zlog_settings = { path = "crates/zlog_settings" }
@@ -426,7 +433,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { path = "../agent-client-protocol" }
agent-client-protocol = { version = "0.2.0-alpha.8", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -440,6 +447,7 @@ async-fs = "2.1"
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
async-recursion = "1.0.0"
async-tar = "0.5.0"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.29.1"
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
@@ -452,12 +460,13 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
] }
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
blake3 = "1.5.3"
bytes = "1.0"
cargo_metadata = "0.19"
@@ -519,7 +528,7 @@ libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" }
lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "0874f8742fe55b4dc94308c1e3c0069710d8eeaf" }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
@@ -531,6 +540,31 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
objc2-foundation = { version = "0.3", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
@@ -556,7 +590,7 @@ prost-build = "0.9"
prost-types = "0.9"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.8.5"
rand = "0.9"
rayon = "1.8"
ref-cast = "1.0.24"
regex = "1.5"
@@ -588,6 +622,7 @@ serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_path_to_error = "0.1.17"
serde_repr = "0.1"
serde_urlencoded = "0.7"
sha2 = "0.10"
@@ -691,6 +726,7 @@ features = [
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Hlsl",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",
@@ -842,6 +878,9 @@ too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
nonminimal_bool = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -16,6 +16,7 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -63,8 +64,8 @@
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
@@ -170,6 +171,7 @@
"context": "Markdown",
"bindings": {
"copy": "markdown::Copy",
"ctrl-insert": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -245,7 +247,10 @@
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
"alt-enter": "agent::ContinueWithBurnMode"
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-d": "agent::RejectOnce"
}
},
{
@@ -258,6 +263,7 @@
"context": "AgentPanel > Markdown",
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-insert": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -325,6 +331,12 @@
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
@@ -342,7 +354,8 @@
"ctrl-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
@@ -483,8 +496,8 @@
"alt-down": "editor::MoveLineDown",
"ctrl-alt-shift-up": "editor::DuplicateLineUp",
"ctrl-alt-shift-down": "editor::DuplicateLineDown",
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"alt-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection
"alt-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
@@ -580,7 +593,7 @@
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-`": "terminal_panel::Toggle",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -625,6 +638,7 @@
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-k ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -1025,6 +1039,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {

View File

@@ -70,9 +70,9 @@
"cmd-k q": "editor::Rewrap",
"cmd-backspace": "editor::DeleteToBeginningOfLine",
"cmd-delete": "editor::DeleteToEndOfLine",
"alt-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"alt-delete": "editor::DeleteToNextWordEnd",
"alt-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"alt-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cmd-x": "editor::Cut",
"cmd-c": "editor::Copy",
"cmd-v": "editor::Paste",
@@ -218,7 +218,7 @@
}
},
{
"context": "Editor && !agent_diff",
"context": "Editor && !agent_diff && !AgentPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-z": "git::Restore",
@@ -286,7 +286,10 @@
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread",
"alt-enter": "agent::ContinueWithBurnMode"
"alt-enter": "agent::ContinueWithBurnMode",
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-d": "agent::RejectOnce"
}
},
{
@@ -378,6 +381,12 @@
"ctrl--": "pane::GoBack"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
"cmd-enter": "menu::Confirm"
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
@@ -385,7 +394,8 @@
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
@@ -395,7 +405,8 @@
"cmd-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
@@ -536,8 +547,10 @@
"alt-down": "editor::MoveLineDown",
"alt-shift-up": "editor::DuplicateLineUp",
"alt-shift-down": "editor::DuplicateLineDown",
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"cmd-ctrl-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
"cmd-ctrl-right": "editor::SelectLargerSyntaxNode", // Expand selection
"cmd-ctrl-up": "editor::SelectPreviousSyntaxNode", // Move selection up
"cmd-ctrl-down": "editor::SelectNextSyntaxNode", // Move selection down
"cmd-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
"cmd-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"cmd-f2": "editor::SelectAllMatches", // Select all occurrences of current word
@@ -649,7 +662,7 @@
"alt-shift-enter": "toast::RunAction",
"cmd-shift-s": "workspace::SaveAs",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-`": "terminal_panel::Toggle",
"cmd-1": ["workspace::ActivatePane", 0],
"cmd-2": ["workspace::ActivatePane", 1],
"cmd-3": ["workspace::ActivatePane", 2],
@@ -690,6 +703,7 @@
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
"cmd-k cmd-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
"cmd-k cmd-right": "workspace::ActivatePaneRight",
@@ -1094,6 +1108,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,

View File

@@ -25,7 +25,6 @@
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"open": "workspace::Open",
"ctrl-o": "workspace::Open",
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
@@ -66,20 +65,15 @@
"ctrl-k": "editor::CutToEndOfLine",
"ctrl-k ctrl-q": "editor::Rewrap",
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
"ctrl-delete": "editor::DeleteToNextWordEnd",
"cut": "editor::Cut",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
"copy": "editor::Copy",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
"paste": "editor::Paste",
"shift-insert": "editor::Paste",
"ctrl-v": "editor::Paste",
"undo": "editor::Undo",
"ctrl-z": "editor::Undo",
"redo": "editor::Redo",
"ctrl-y": "editor::Redo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
@@ -138,7 +132,6 @@
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "assistant::QuoteSelection",
@@ -177,7 +170,6 @@
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -225,7 +217,6 @@
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -258,7 +249,10 @@
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
"alt-enter": "agent::ContinueWithBurnMode"
"alt-enter": "agent::ContinueWithBurnMode",
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-d": "agent::RejectOnce"
}
},
{
@@ -272,7 +266,6 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -346,6 +339,12 @@
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
@@ -353,7 +352,8 @@
"enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
@@ -367,7 +367,6 @@
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
}
@@ -381,7 +380,6 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPreviousMatch",
"alt-enter": "search::SelectAllMatches",
"find": "search::FocusSearch",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
@@ -408,7 +406,6 @@
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-r": "search::ToggleRegex" // vscode
@@ -472,14 +469,12 @@
"forward": "pane::GoForward",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPreviousMatch",
"shift-find": "project_search::ToggleFocus",
"ctrl-shift-f": "project_search::ToggleFocus",
"shift-alt-h": "search::ToggleReplace",
"alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-f": "project_search::ToggleFilters",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
@@ -500,8 +495,10 @@
"alt-down": "editor::MoveLineDown",
"shift-alt-up": "editor::DuplicateLineUp",
"shift-alt-down": "editor::DuplicateLineDown",
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection
"shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand selection
"shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink selection
"ctrl-shift-right": "editor::SelectLargerSyntaxNode", // Expand selection (VSCode version)
"ctrl-shift-left": "editor::SelectSmallerSyntaxNode", // Shrink selection (VSCode version)
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
@@ -579,27 +576,21 @@
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
// Change the default action on `menu::Confirm` by setting the parameter
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
// Change to open path modal for existing remote connection by setting the parameter
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"shift-alt-b": "branches::OpenRecent",
"shift-alt-enter": "toast::RunAction",
"ctrl-shift-`": "workspace::NewTerminal",
"save": "workspace::Save",
"ctrl-s": "workspace::Save",
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
"shift-save": "workspace::SaveAs",
"ctrl-shift-s": "workspace::SaveAs",
"new": "workspace::NewFile",
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::ToggleFocus",
"ctrl-`": "terminal_panel::Toggle",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -621,7 +612,6 @@
"shift-alt-0": "workspace::ResetOpenDocksSize",
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
@@ -641,9 +631,9 @@
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-shift-/": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-k s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-m ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -848,9 +838,7 @@
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"alt-copy": "outline_panel::CopyPath",
"shift-alt-c": "outline_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
"ctrl-alt-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::OpenSelectedEntry",
@@ -866,21 +854,14 @@
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
"alt-new": "project_panel::NewDirectory",
"alt-n": "project_panel::NewDirectory",
"cut": "project_panel::Cut",
"ctrl-x": "project_panel::Cut",
"copy": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-c": "project_panel::Copy",
"paste": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-v": "project_panel::Paste",
"alt-copy": "project_panel::CopyPath",
"shift-alt-c": "project_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
@@ -892,7 +873,6 @@
"ctrl-alt-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
@@ -1075,6 +1055,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
@@ -1110,10 +1097,8 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"copy": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"ctrl-shift-c": "terminal::Copy",
"paste": "terminal::Paste",
"shift-insert": "terminal::Paste",
"ctrl-shift-v": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
@@ -1129,7 +1114,6 @@
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
"ctrl-shift-l": "terminal::Clear",
"ctrl-shift-w": "pane::CloseActiveItem",
@@ -1210,7 +1194,6 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch",
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
"alt-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",

View File

@@ -42,7 +42,7 @@
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save

View File

@@ -125,7 +125,7 @@
{
"context": "Workspace || Editor",
"bindings": {
"alt-f12": "terminal_panel::ToggleFocus",
"alt-f12": "terminal_panel::Toggle",
"ctrl-shift-k": "git::Push"
}
},

View File

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

View File

@@ -42,7 +42,7 @@
"alt-,": "pane::GoBack", // xref-pop-marker-stack
"ctrl-x h": "editor::SelectAll", // mark-whole-buffer
"ctrl-d": "editor::Delete", // delete-char
"alt-d": "editor::DeleteToNextWordEnd", // kill-word
"alt-d": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }], // kill-word
"ctrl-k": "editor::KillRingCut", // kill-line
"ctrl-w": "editor::Cut", // kill-region
"alt-w": "editor::Copy", // kill-ring-save

View File

@@ -127,7 +127,7 @@
{
"context": "Workspace || Editor",
"bindings": {
"alt-f12": "terminal_panel::ToggleFocus",
"alt-f12": "terminal_panel::Toggle",
"cmd-shift-k": "git::Push"
}
},

View File

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

View File

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

View File

@@ -32,34 +32,6 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "vim::GoToColumn",
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] shift-m": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ shift-m": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
"[ -": "vim::PreviousLesserIndent",
"[ +": "vim::PreviousGreaterIndent",
"[ =": "vim::PreviousSameIndent",
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
"] b": "pane::ActivateNextItem",
"[ b": "pane::ActivatePreviousItem",
"] shift-b": "pane::ActivateLastItem",
"[ shift-b": ["pane::ActivateItem", 0],
"] space": "vim::InsertEmptyLineBelow",
"[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown",
"[ f": "workspace::FollowNextCollaborator",
"] f": "workspace::FollowNextCollaborator",
// Word motions
"w": "vim::NextWordStart",
@@ -83,10 +55,6 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPreviousMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
@@ -219,6 +187,46 @@
".": "vim::Repeat"
}
},
{
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
"bindings": {
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] shift-m": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ shift-m": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
"[ -": "vim::PreviousLesserIndent",
"[ +": "vim::PreviousGreaterIndent",
"[ =": "vim::PreviousSameIndent",
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
"] b": "pane::ActivateNextItem",
"[ b": "pane::ActivatePreviousItem",
"] shift-b": "pane::ActivateLastItem",
"[ shift-b": ["pane::ActivateItem", 0],
"] space": "vim::InsertEmptyLineBelow",
"[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown",
"[ f": "workspace::FollowNextCollaborator",
"] f": "workspace::FollowNextCollaborator",
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode"
}
},
{
"context": "vim_mode == normal",
"bindings": {
@@ -249,9 +257,6 @@
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
@@ -317,10 +322,7 @@
"g w": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
// "g ?": "vim::ConvertToRot47",
"\"": "vim::PushRegister",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
"\"": "vim::PushRegister"
}
},
{
@@ -337,7 +339,7 @@
"ctrl-x ctrl-z": "editor::Cancel",
"ctrl-x ctrl-e": "vim::LineDown",
"ctrl-x ctrl-y": "vim::LineUp",
"ctrl-w": "editor::DeleteToPreviousWordStart",
"ctrl-w": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-u": "editor::DeleteToBeginningOfLine",
"ctrl-t": "vim::Indent",
"ctrl-d": "vim::Outdent",
@@ -354,6 +356,15 @@
"ctrl-s": "editor::ShowSignatureHelp"
}
},
{
"context": "showing_completions",
"bindings": {
"ctrl-d": "vim::ScrollDown",
"ctrl-u": "vim::ScrollUp",
"ctrl-e": "vim::LineDown",
"ctrl-y": "vim::LineUp"
}
},
{
"context": "(vim_mode == normal || vim_mode == helix_normal) && !menu",
"bindings": {
@@ -388,6 +399,9 @@
"ctrl-[": "editor::Cancel",
";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle",
"m": "vim::PushHelixMatch",
"]": ["vim::PushHelixNext", { "around": true }],
"[": ["vim::PushHelixPrevious", { "around": true }],
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
@@ -410,13 +424,6 @@
"insert": "vim::InsertBefore",
"alt-.": "vim::RepeatFind",
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPreviousHunk",
// Goto mode
"g n": "pane::ActivateNextItem",
"g p": "pane::ActivatePreviousItem",
@@ -460,9 +467,6 @@
"space c": "editor::ToggleComments",
"space y": "editor::Copy",
"space p": "editor::Paste",
// Match mode
"m m": "vim::Matching",
"m i w": ["workspace::SendKeystrokes", "v i w"],
"shift-u": "editor::Redo",
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
@@ -531,7 +535,7 @@
}
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
@@ -568,6 +572,48 @@
"e": "vim::EntireFile"
}
},
{
"context": "vim_operator == helix_m",
"bindings": {
"m": "vim::Matching"
}
},
{
"context": "vim_operator == helix_next",
"bindings": {
"z": "vim::NextSectionStart",
"shift-z": "vim::NextSectionEnd",
"*": "vim::NextComment",
"/": "vim::NextComment",
"-": "vim::NextLesserIndent",
"+": "vim::NextGreaterIndent",
"=": "vim::NextSameIndent",
"b": "pane::ActivateNextItem",
"shift-b": "pane::ActivateLastItem",
"x": "editor::SelectSmallerSyntaxNode",
"d": "editor::GoToDiagnostic",
"c": "editor::GoToHunk",
"space": "vim::InsertEmptyLineBelow"
}
},
{
"context": "vim_operator == helix_previous",
"bindings": {
"z": "vim::PreviousSectionStart",
"shift-z": "vim::PreviousSectionEnd",
"*": "vim::PreviousComment",
"/": "vim::PreviousComment",
"-": "vim::PreviousLesserIndent",
"+": "vim::PreviousGreaterIndent",
"=": "vim::PreviousSameIndent",
"b": "pane::ActivatePreviousItem",
"shift-b": ["pane::ActivateItem", 0],
"x": "editor::SelectLargerSyntaxNode",
"d": "editor::GoToPreviousDiagnostic",
"c": "editor::GoToPreviousHunk",
"space": "vim::InsertEmptyLineAbove"
}
},
{
"context": "vim_operator == c",
"bindings": {
@@ -814,11 +860,11 @@
"j": "menu::SelectNext",
"k": "menu::SelectPrevious",
"l": "project_panel::ExpandSelectedEntry",
"o": "project_panel::OpenPermanent",
"shift-d": "project_panel::Delete",
"shift-r": "project_panel::Rename",
"t": "project_panel::OpenPermanent",
"v": "project_panel::OpenPermanent",
"v": "project_panel::OpenSplitVertical",
"o": "project_panel::OpenSplitHorizontal",
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
"s": "workspace::OpenWithSystem",

View File

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

View File

@@ -1,4 +1,5 @@
{
"project_name": null,
// The name of the Zed theme to use for the UI.
//
// `mode` is one of:
@@ -188,8 +189,8 @@
// 4. A box drawn around the following character
// "hollow"
//
// Default: not set, defaults to "bar"
"cursor_shape": null,
// Default: "bar"
"cursor_shape": "bar",
// Determines when the mouse cursor should be hidden in an editor or input box.
//
// 1. Never hide the mouse cursor:
@@ -223,9 +224,25 @@
"current_line_highlight": "all",
// Whether to highlight all occurrences of the selected text in an editor.
"selection_highlight": true,
// Whether the text selection should have rounded corners.
"rounded_selection": true,
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// The minimum APCA perceptual contrast between foreground and background colors.
// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x,
// especially for dark mode. Values range from 0 to 106.
//
// Based on APCA Readability Criterion (ARC) Bronze Simple Mode:
// https://readtech.org/ARC/tests/bronze-simple-mode/
// - 0: No contrast adjustment
// - 45: Minimum for large fluent text (36px+)
// - 60: Minimum for other content text
// - 75: Minimum for body text
// - 90: Preferred for body text
//
// This only affects text drawn over highlight backgrounds in the editor.
"minimum_contrast_for_highlights": 45,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
@@ -266,8 +283,8 @@
// - "warning"
// - "info"
// - "hint"
// - null — allow all diagnostics (default)
"diagnostics_max_severity": null,
// - "all" — allow all diagnostics (default)
"diagnostics_max_severity": "all",
// Whether to show wrap guides (vertical rulers) in the editor.
// Setting this to true will show a guide at the 'preferred_line_length' value
// if 'soft_wrap' is set to 'preferred_line_length', and will show any
@@ -279,6 +296,8 @@
"redact_private_values": false,
// The default number of lines to expand excerpts in the multibuffer by.
"expand_excerpt_lines": 5,
// The default number of context lines shown in multibuffer excerpts.
"excerpt_context_lines": 2,
// Globs to match against file paths to determine if a file is private.
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
// Whether to use additional LSP queries to format (and amend) the code after
@@ -722,16 +741,6 @@
// Default width of the collaboration panel.
"default_width": 240
},
"chat_panel": {
// When to show the chat panel button in the status bar.
// Can be 'never', 'always', or 'when_in_call',
// or a boolean (interpreted as 'never'/'always').
"button": "when_in_call",
// Where to dock the chat panel. Can be 'left' or 'right'.
"dock": "right",
// Default width of the chat panel.
"default_width": 240
},
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
@@ -820,6 +829,9 @@
// }
],
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
//
// Note: This setting has no effect on external agents that support permission modes, such as Claude Code.
// You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests.
"always_allow_tool_actions": false,
// When enabled, the agent will stream edits.
"stream_edits": false,
@@ -944,7 +956,7 @@
// Show git status colors in the editor tabs.
"git_status": false,
// Position of the close button on the editor tabs.
// One of: ["right", "left", "hidden"]
// One of: ["right", "left"]
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,
@@ -1187,6 +1199,10 @@
// The minimum column number to show the inline blame information at
"min_column": 0
},
// Control which information is shown in the branch picker.
"branch_picker": {
"show_author_name": true
},
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
@@ -1585,7 +1601,7 @@
"ensure_final_newline_on_save": false
},
"Elixir": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"Elm": {
"tab_size": 4
@@ -1610,7 +1626,7 @@
}
},
"HEEX": {
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
"language_servers": ["elixir-ls", "!expert", "!next-ls", "!lexical", "..."]
},
"HTML": {
"prettier": {
@@ -1758,7 +1774,7 @@
"api_url": "http://localhost:1234/api/v0"
},
"deepseek": {
"api_url": "https://api.deepseek.com"
"api_url": "https://api.deepseek.com/v1"
},
"mistral": {
"api_url": "https://api.mistral.ai/v1"
@@ -1906,7 +1922,10 @@
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
"timeout": 2000,
"dock": "bottom",
"log_dap_communications": true,
"format_dap_log_messages": true,
"button": true
},
// Configures any number of settings profiles that are temporarily applied on

View File

@@ -316,6 +316,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#a6a5a0ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#d2a6ffff",
"font_style": null,
@@ -702,6 +707,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#73777bff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#a37accff",
"font_style": null,
@@ -1088,6 +1098,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#b4b3aeff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#dfbfffff",
"font_style": null,

View File

@@ -325,6 +325,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#e5d5adff",
"font_style": null,
@@ -725,6 +730,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#e5d5adff",
"font_style": null,
@@ -1125,6 +1135,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#83a598ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#e5d5adff",
"font_style": null,
@@ -1525,6 +1540,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#413d3aff",
"font_style": null,
@@ -1925,6 +1945,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#413d3aff",
"font_style": null,
@@ -2325,6 +2350,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#066578ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#413d3aff",
"font_style": null,

View File

@@ -321,6 +321,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#d07277ff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#b1574bff",
"font_style": null,
@@ -715,6 +720,11 @@
"font_style": null,
"font_weight": null
},
"punctuation.markup": {
"color": "#d3604fff",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#b92b46ff",
"font_style": null,

View File

@@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
@@ -30,18 +31,21 @@ language.workspace = true
language_model.workspace = true
markdown.workspace = true
parking_lot = { workspace = true, optional = true }
portable-pty.workspace = true
project.workspace = true
prompt_store.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
task.workspace = true
terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

File diff suppressed because it is too large Load Diff

View File

@@ -76,8 +76,13 @@ pub trait AgentConnection {
None
}
fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>>;
fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>>;
fn session_modes(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionModes>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -113,6 +118,14 @@ pub trait AgentTelemetry {
) -> Task<Result<serde_json::Value>>;
}
pub trait AgentSessionModes {
fn current_mode(&self) -> acp::SessionModeId;
fn all_modes(&self) -> Vec<acp::SessionMode>;
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
}
#[derive(Debug)]
pub struct AuthRequired {
pub description: Option<String>,
@@ -341,7 +354,6 @@ mod test_support {
image: true,
audio: true,
embedded_context: true,
supports_custom_commands: false,
}),
cx,
)
@@ -397,14 +409,16 @@ mod test_support {
};
let task = cx.spawn(async move |cx| {
if let Some((tool_call, options)) = permission_request {
let permission = thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
cx,
)
})?;
permission?.await?;
thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
false,
cx,
)
})??
.await;
}
thread.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
@@ -444,14 +458,6 @@ mod test_support {
Some(Rc::new(StubAgentSessionEditor))
}
fn list_commands(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
Task::ready(Ok(acp::ListCommandsResponse { commands: vec![] }))
}
fn run_command(&self, _request: acp::RunCommandRequest, _cx: &mut App) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ use crate::{
ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization,
UserMessageContent, templates::Templates,
};
use crate::{HistoryStore, TitleUpdated, TokenUsageUpdated};
use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated};
use acp_thread::{AcpThread, AgentModelSelector};
use action_log::ActionLog;
use agent_client_protocol as acp;
@@ -10,7 +10,8 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashSet, IndexMap};
use fs::Fs;
use futures::channel::mpsc;
use futures::channel::{mpsc, oneshot};
use futures::future::Shared;
use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
@@ -23,7 +24,7 @@ use prompt_store::{
use settings::update_settings_file;
use std::any::Any;
use std::collections::HashMap;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use util::ResultExt;
@@ -276,13 +277,6 @@ impl NativeAgent {
cx: &mut Context<Self>,
) -> Entity<AcpThread> {
let connection = Rc::new(NativeAgentConnection(cx.entity()));
let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
thread.add_default_tools(cx)
});
let thread = thread_handle.read(cx);
let session_id = thread.id().clone();
@@ -301,6 +295,20 @@ impl NativeAgent {
cx,
)
});
let registry = LanguageModelRegistry::read_global(cx);
let summarization_model = registry.thread_summary_model().map(|c| c.model);
thread_handle.update(cx, |thread, cx| {
thread.set_summarization_model(summarization_model, cx);
thread.add_default_tools(
Rc::new(AcpThreadEnvironment {
acp_thread: acp_thread.downgrade(),
}) as _,
cx,
)
});
let subscriptions = vec![
cx.observe_release(&acp_thread, |this, acp_thread, _cx| {
this.sessions.remove(acp_thread.session_id());
@@ -762,18 +770,17 @@ impl NativeAgentConnection {
options,
response,
}) => {
let recv = acp_thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call, options, cx)
})?;
let outcome_task = acp_thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
tool_call, options, true, cx,
)
})??;
cx.background_spawn(async move {
if let Some(recv) = recv.log_err()
&& let Some(option) = recv
.await
.context("authorization sender was dropped")
.log_err()
if let acp::RequestPermissionOutcome::Selected { option_id } =
outcome_task.await
{
response
.send(option)
.send(option_id)
.map(|_| anyhow!("authorization receiver was dropped"))
.log_err();
}
@@ -1004,7 +1011,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
) -> Option<Rc<dyn acp_thread::AgentSessionTruncate>> {
self.0.read_with(cx, |agent, _cx| {
agent.sessions.get(session_id).map(|session| {
Rc::new(NativeAgentSessionEditor {
Rc::new(NativeAgentSessionTruncate {
thread: session.thread.clone(),
acp_thread: session.acp_thread.clone(),
}) as _
@@ -1027,19 +1034,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
Some(Rc::new(self.clone()) as Rc<dyn acp_thread::AgentTelemetry>)
}
fn list_commands(&self, session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
// Native agent doesn't support custom commands yet
let _session_id = session_id.clone();
Task::ready(Ok(acp::ListCommandsResponse {
commands: vec![],
}))
}
fn run_command(&self, _request: acp::RunCommandRequest, _cx: &mut App) -> Task<Result<()>> {
// Native agent doesn't support custom commands yet
Task::ready(Err(anyhow!("Custom commands not supported")))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
@@ -1066,12 +1060,12 @@ impl acp_thread::AgentTelemetry for NativeAgentConnection {
}
}
struct NativeAgentSessionEditor {
struct NativeAgentSessionTruncate {
thread: Entity<Thread>,
acp_thread: WeakEntity<AcpThread>,
}
impl acp_thread::AgentSessionTruncate for NativeAgentSessionEditor {
impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate {
fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
match self.thread.update(cx, |thread, cx| {
thread.truncate(message_id.clone(), cx)?;
@@ -1120,6 +1114,66 @@ impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle {
}
}
pub struct AcpThreadEnvironment {
acp_thread: WeakEntity<AcpThread>,
}
impl ThreadEnvironment for AcpThreadEnvironment {
fn create_terminal(
&self,
command: String,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
cx: &mut AsyncApp,
) -> Task<Result<Rc<dyn TerminalHandle>>> {
let task = self.acp_thread.update(cx, |thread, cx| {
thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx)
});
let acp_thread = self.acp_thread.clone();
cx.spawn(async move |cx| {
let terminal = task?.await?;
let (drop_tx, drop_rx) = oneshot::channel();
let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?;
cx.spawn(async move |cx| {
drop_rx.await.ok();
acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx))
})
.detach();
let handle = AcpTerminalHandle {
terminal,
_drop_tx: Some(drop_tx),
};
Ok(Rc::new(handle) as _)
})
}
}
pub struct AcpTerminalHandle {
terminal: Entity<acp_thread::Terminal>,
_drop_tx: Option<oneshot::Sender<()>>,
}
impl TerminalHandle for AcpTerminalHandle {
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId> {
self.terminal.read_with(cx, |term, _cx| term.id().clone())
}
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>> {
self.terminal
.read_with(cx, |term, _cx| term.wait_for_exit())
}
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse> {
self.terminal
.read_with(cx, |term, cx| term.current_output(cx))
}
}
#[cfg(test)]
mod tests {
use crate::HistoryEntryId;

View File

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

View File

@@ -35,10 +35,15 @@ impl AgentServer for NativeAgentServer {
fn connect(
&self,
_root_dir: &Path,
_root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
) -> Task<
Result<(
Rc<dyn acp_thread::AgentConnection>,
Option<task::SpawnInTerminal>,
)>,
> {
log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
@@ -60,7 +65,10 @@ impl AgentServer for NativeAgentServer {
let connection = NativeAgentConnection(agent);
log::debug!("NativeAgentServer connection established successfully");
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
Ok((
Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>,
None,
))
})
}

View File

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

View File

@@ -24,7 +24,11 @@ impl AgentTool for EchoTool {
acp::ToolKind::Other
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"Echo".into()
}
@@ -55,7 +59,11 @@ impl AgentTool for DelayTool {
"delay"
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
format!("Delay {}ms", input.ms).into()
} else {
@@ -100,7 +108,11 @@ impl AgentTool for ToolRequiringPermission {
acp::ToolKind::Other
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"This tool requires permission".into()
}
@@ -135,7 +147,11 @@ impl AgentTool for InfiniteTool {
acp::ToolKind::Other
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"Infinite Tool".into()
}
@@ -186,7 +202,11 @@ impl AgentTool for WordListTool {
acp::ToolKind::Other
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"List of random words".into()
}

View File

@@ -45,14 +45,15 @@ use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, update_settings_file};
use smol::stream::StreamExt;
use std::fmt::Write;
use std::{
collections::BTreeMap,
ops::RangeInclusive,
path::Path,
rc::Rc,
sync::Arc,
time::{Duration, Instant},
};
use std::{fmt::Write, path::PathBuf};
use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
use uuid::Uuid;
@@ -484,11 +485,15 @@ impl AgentMessage {
};
for tool_result in self.tool_results.values() {
let mut tool_result = tool_result.clone();
// Surprisingly, the API fails if we return an empty string here.
// It thinks we are sending a tool use without a tool result.
if tool_result.content.is_empty() {
tool_result.content = "<Tool returned an empty string>".into();
}
user_message
.content
.push(language_model::MessageContent::ToolResult(
tool_result.clone(),
));
.push(language_model::MessageContent::ToolResult(tool_result));
}
let mut messages = Vec::new();
@@ -519,6 +524,22 @@ pub enum AgentMessageContent {
ToolUse(LanguageModelToolUse),
}
pub trait TerminalHandle {
fn id(&self, cx: &AsyncApp) -> Result<acp::TerminalId>;
fn current_output(&self, cx: &AsyncApp) -> Result<acp::TerminalOutputResponse>;
fn wait_for_exit(&self, cx: &AsyncApp) -> Result<Shared<Task<acp::TerminalExitStatus>>>;
}
pub trait ThreadEnvironment {
fn create_terminal(
&self,
command: String,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
cx: &mut AsyncApp,
) -> Task<Result<Rc<dyn TerminalHandle>>>;
}
#[derive(Debug)]
pub enum ThreadEvent {
UserMessage(UserMessage),
@@ -531,6 +552,14 @@ pub enum ThreadEvent {
Stop(acp::StopReason),
}
#[derive(Debug)]
pub struct NewTerminal {
pub command: String,
pub output_byte_limit: Option<u64>,
pub cwd: Option<PathBuf>,
pub response: oneshot::Sender<Result<Entity<acp_thread::Terminal>>>,
}
#[derive(Debug)]
pub struct ToolCallAuthorization {
pub tool_call: acp::ToolCallUpdate,
@@ -588,7 +617,6 @@ impl Thread {
image,
audio: false,
embedded_context: true,
supports_custom_commands: false,
}
}
@@ -713,7 +741,7 @@ impl Thread {
return;
};
let title = tool.initial_title(tool_use.input.clone());
let title = tool.initial_title(tool_use.input.clone(), cx);
let kind = tool.kind();
stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone());
@@ -1021,7 +1049,11 @@ impl Thread {
}
}
pub fn add_default_tools(&mut self, cx: &mut Context<Self>) {
pub fn add_default_tools(
&mut self,
environment: Rc<dyn ThreadEnvironment>,
cx: &mut Context<Self>,
) {
let language_registry = self.project.read(cx).languages().clone();
self.add_tool(CopyPathTool::new(self.project.clone()));
self.add_tool(CreateDirectoryTool::new(self.project.clone()));
@@ -1030,7 +1062,11 @@ impl Thread {
self.action_log.clone(),
));
self.add_tool(DiagnosticsTool::new(self.project.clone()));
self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry));
self.add_tool(EditFileTool::new(
self.project.clone(),
cx.weak_entity(),
language_registry,
));
self.add_tool(FetchTool::new(self.project.read(cx).client().http_client()));
self.add_tool(FindPathTool::new(self.project.clone()));
self.add_tool(GrepTool::new(self.project.clone()));
@@ -1042,7 +1078,7 @@ impl Thread {
self.project.clone(),
self.action_log.clone(),
));
self.add_tool(TerminalTool::new(self.project.clone(), cx));
self.add_tool(TerminalTool::new(self.project.clone(), environment));
self.add_tool(ThinkingTool);
self.add_tool(WebSearchTool);
}
@@ -1482,7 +1518,7 @@ impl Thread {
let mut title = SharedString::from(&tool_use.name);
let mut kind = acp::ToolKind::Other;
if let Some(tool) = tool.as_ref() {
title = tool.initial_title(tool_use.input.clone());
title = tool.initial_title(tool_use.input.clone(), cx);
kind = tool.kind();
}
@@ -2116,7 +2152,11 @@ where
fn kind() -> acp::ToolKind;
/// The initial tool title to display. Can be updated during the tool run.
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString;
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
cx: &mut App,
) -> SharedString;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema {
@@ -2164,7 +2204,7 @@ pub trait AnyAgentTool {
fn name(&self) -> SharedString;
fn description(&self) -> SharedString;
fn kind(&self) -> acp::ToolKind;
fn initial_title(&self, input: serde_json::Value) -> SharedString;
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString;
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool {
true
@@ -2200,9 +2240,9 @@ where
T::kind()
}
fn initial_title(&self, input: serde_json::Value) -> SharedString {
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString {
let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input);
self.0.initial_title(parsed_input)
self.0.initial_title(parsed_input, _cx)
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
@@ -2386,19 +2426,6 @@ impl ToolCallEventStream {
.ok();
}
pub fn update_terminal(&self, terminal: Entity<acp_thread::Terminal>) {
self.stream
.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateTerminal {
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
terminal,
}
.into(),
)))
.ok();
}
pub fn authorize(&self, title: impl Into<String>, cx: &mut App) -> Task<Result<()>> {
if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions {
return Task::ready(Ok(()));

View File

@@ -145,7 +145,7 @@ impl AnyAgentTool for ContextServerTool {
ToolKind::Other
}
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString {
format!("Run MCP tool `{}`", self.tool.name).into()
}
@@ -169,15 +169,18 @@ impl AnyAgentTool for ContextServerTool {
fn run(
self: Arc<Self>,
input: serde_json::Value,
_event_stream: ToolCallEventStream,
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<AgentToolOutput>> {
let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx);
cx.spawn(async move |_cx| {
authorize.await?;
let Some(protocol) = server.client() else {
bail!("Context server not initialized");
};

View File

@@ -58,7 +58,11 @@ impl AgentTool for CopyPathTool {
ToolKind::Move
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> ui::SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);

View File

@@ -49,7 +49,11 @@ impl AgentTool for CreateDirectoryTool {
ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
} else {

View File

@@ -52,7 +52,11 @@ impl AgentTool for DeletePathTool {
ToolKind::Delete
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
format!("Delete “`{}`”", input.path).into()
} else {

View File

@@ -71,7 +71,11 @@ impl AgentTool for DiagnosticsTool {
acp::ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Some(path) = input.ok().and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(path),
_ => None,

View File

@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
#[schemars(inline)]
pub enum EditFileMode {
Edit,
Create,
@@ -119,11 +120,17 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
pub struct EditFileTool {
thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>,
project: Entity<Project>,
}
impl EditFileTool {
pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
pub fn new(
project: Entity<Project>,
thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>,
) -> Self {
Self {
project,
thread,
language_registry,
}
@@ -194,22 +201,50 @@ impl AgentTool for EditFileTool {
acp::ToolKind::Edit
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
cx: &mut App,
) -> SharedString {
match input {
Ok(input) => input.display_description.into(),
Ok(input) => self
.project
.read(cx)
.find_project_path(&input.path, cx)
.and_then(|project_path| {
self.project
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.into(),
Err(raw_input) => {
if let Some(input) =
serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
{
let path = input.path.trim();
if !path.is_empty() {
return self
.project
.read(cx)
.find_project_path(&input.path, cx)
.and_then(|project_path| {
self.project
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.into();
}
let description = input.display_description.trim();
if !description.is_empty() {
return description.to_string().into();
}
let path = input.path.trim().to_string();
if !path.is_empty() {
return path.into();
}
}
DEFAULT_UI_TEXT.into()
@@ -544,7 +579,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -559,11 +594,12 @@ mod tests {
path: "root/nonexistent_file.txt".into(),
mode: EditFileMode::Edit,
};
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
Arc::new(EditFileTool::new(
project,
thread.downgrade(),
language_registry,
))
.run(input, ToolCallEventStream::test().0, cx)
})
.await;
assert_eq!(
@@ -742,7 +778,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -774,6 +810,7 @@ mod tests {
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry.clone(),
))
@@ -832,11 +869,12 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
))
.run(input, ToolCallEventStream::test().0, cx)
});
// Stream the unformatted content
@@ -884,7 +922,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -917,6 +955,7 @@ mod tests {
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry.clone(),
))
@@ -968,11 +1007,12 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
))
.run(input, ToolCallEventStream::test().0, cx)
});
// Stream the content with trailing whitespace
@@ -1011,7 +1051,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -1019,7 +1059,11 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation
@@ -1147,7 +1191,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project,
project.clone(),
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -1155,7 +1199,11 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
// Test global config paths - these should require confirmation if they exist and are outside the project
let test_cases = vec![
@@ -1263,7 +1311,11 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
// Test files in different worktrees
let test_cases = vec![
@@ -1343,7 +1395,11 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
// Test edge cases
let test_cases = vec![
@@ -1426,7 +1482,11 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
// Test different EditFileMode values
let modes = vec![
@@ -1506,48 +1566,67 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
let tool = Arc::new(EditFileTool::new(
project,
thread.downgrade(),
language_registry,
));
assert_eq!(
tool.initial_title(Err(json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
}))),
"src/main.rs"
);
assert_eq!(
tool.initial_title(Err(json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
}))),
"Fix error handling"
);
assert_eq!(
tool.initial_title(Err(json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
}))),
"Fix error handling"
);
assert_eq!(
tool.initial_title(Err(json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
}))),
DEFAULT_UI_TEXT
);
assert_eq!(
tool.initial_title(Err(serde_json::Value::Null)),
DEFAULT_UI_TEXT
);
cx.update(|cx| {
// ...
assert_eq!(
tool.initial_title(
Err(json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
"src/main.rs"
);
assert_eq!(
tool.initial_title(
Err(json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
"Fix error handling"
);
assert_eq!(
tool.initial_title(
Err(json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
"src/main.rs"
);
assert_eq!(
tool.initial_title(
Err(json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
DEFAULT_UI_TEXT
);
assert_eq!(
tool.initial_title(Err(serde_json::Value::Null), cx),
DEFAULT_UI_TEXT
);
});
}
#[gpui::test]
@@ -1574,7 +1653,11 @@ mod tests {
// Ensure the diff is finalized after the edit completes.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages.clone(),
));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
@@ -1599,7 +1682,11 @@ mod tests {
// Ensure the diff is finalized if an error occurs while editing.
{
model.forbid_requests();
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages.clone(),
));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
@@ -1622,7 +1709,11 @@ mod tests {
// Ensure the diff is finalized if the tool call gets dropped.
{
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages.clone(),
));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(

View File

@@ -126,7 +126,11 @@ impl AgentTool for FetchTool {
acp::ToolKind::Fetch
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
match input {
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
Err(_) => "Fetch URL".into(),

View File

@@ -93,7 +93,11 @@ impl AgentTool for FindPathTool {
acp::ToolKind::Search
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
let mut title = "Find paths".to_string();
if let Ok(input) = input {
title.push_str(&format!(" matching “`{}`”", input.glob));

View File

@@ -75,7 +75,11 @@ impl AgentTool for GrepTool {
acp::ToolKind::Search
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
match input {
Ok(input) => {
let page = input.page();

View File

@@ -59,7 +59,11 @@ impl AgentTool for ListDirectoryTool {
ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
let path = MarkdownInlineCode(&input.path);
format!("List the {path} directory's contents").into()

View File

@@ -60,7 +60,11 @@ impl AgentTool for MovePathTool {
ToolKind::Move
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);

View File

@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[schemars(inline)]
pub enum Timezone {
/// Use UTC for the datetime.
Utc,
@@ -40,7 +41,11 @@ impl AgentTool for NowTool {
acp::ToolKind::Other
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"Get current time".into()
}

View File

@@ -45,7 +45,11 @@ impl AgentTool for OpenTool {
ToolKind::Execute
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
} else {
@@ -61,7 +65,7 @@ impl AgentTool for OpenTool {
) -> Task<Result<Self::Output>> {
// If path_or_url turns out to be a path in the project, make it absolute.
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
cx.background_spawn(async move {
authorize.await?;

View File

@@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::{path::Path, sync::Arc};
use std::sync::Arc;
use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream};
@@ -68,13 +68,31 @@ impl AgentTool for ReadFileTool {
acp::ToolKind::Read
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
input
.ok()
.as_ref()
.and_then(|input| Path::new(&input.path).file_name())
.map(|file_name| file_name.to_string_lossy().to_string().into())
.unwrap_or_default()
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
cx: &mut App,
) -> SharedString {
if let Ok(input) = input
&& let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx)
&& let Some(path) = self
.project
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
{
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
}
(Some(start), None) => {
format!("Read file `{}` (from line {})", path.display(), start)
}
_ => format!("Read file `{}`", path.display()),
}
.into()
} else {
"Read file".into()
}
}
fn run(
@@ -86,6 +104,12 @@ impl AgentTool for ReadFileTool {
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
};
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
return Task::ready(Err(anyhow!(
"Failed to convert {} to absolute path",
&input.path
)));
};
// Error out if this path is either excluded or private in global settings
let global_settings = WorktreeSettings::get_global(cx);
@@ -121,6 +145,14 @@ impl AgentTool for ReadFileTool {
let file_path = input.path.clone();
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: input.start_line.map(|line| line.saturating_sub(1)),
}]),
..Default::default()
});
if image_store::is_image_file(&self.project, &project_path, cx) {
return cx.spawn(async move |cx| {
let image_entity: Entity<ImageItem> = cx
@@ -229,34 +261,25 @@ impl AgentTool for ReadFileTool {
};
project.update(cx, |project, cx| {
if let Some(abs_path) = project.absolute_path(&project_path, cx) {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor.unwrap_or(text::Anchor::MIN),
}),
cx,
);
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor.unwrap_or(text::Anchor::MIN),
}),
cx,
);
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: input.start_line.map(|line| line.saturating_sub(1)),
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
});
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
})
}
})?;

View File

@@ -1,19 +1,19 @@
use agent_client_protocol as acp;
use anyhow::Result;
use futures::{FutureExt as _, future::Shared};
use gpui::{App, AppContext, Entity, SharedString, Task};
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::{ResultExt, get_system_shell, markdown::MarkdownInlineCode};
use util::markdown::MarkdownInlineCode;
use crate::{AgentTool, ToolCallEventStream};
use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream};
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024;
/// Executes a shell one-liner and returns the combined output.
///
@@ -36,25 +36,14 @@ pub struct TerminalToolInput {
pub struct TerminalTool {
project: Entity<Project>,
determine_shell: Shared<Task<String>>,
environment: Rc<dyn ThreadEnvironment>,
}
impl TerminalTool {
pub fn new(project: Entity<Project>, cx: &mut App) -> Self {
let determine_shell = cx.background_spawn(async move {
if cfg!(windows) {
return get_system_shell();
}
if which::which("bash").is_ok() {
"bash".into()
} else {
get_system_shell()
}
});
pub fn new(project: Entity<Project>, environment: Rc<dyn ThreadEnvironment>) -> Self {
Self {
project,
determine_shell: determine_shell.shared(),
environment,
}
}
}
@@ -71,7 +60,11 @@ impl AgentTool for TerminalTool {
acp::ToolKind::Execute
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
if let Ok(input) = input {
let mut lines = input.command.lines();
let first_line = lines.next().unwrap_or_default();
@@ -99,128 +92,49 @@ impl AgentTool for TerminalTool {
event_stream: ToolCallEventStream,
cx: &mut App,
) -> Task<Result<Self::Output>> {
let language_registry = self.project.read(cx).languages().clone();
let working_dir = match working_dir(&input, &self.project, cx) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(err)),
};
let program = self.determine_shell.clone();
let command = if cfg!(windows) {
format!("$null | & {{{}}}", input.command.replace("\"", "'"))
} else if let Some(cwd) = working_dir
.as_ref()
.and_then(|cwd| cwd.as_os_str().to_str())
{
// Make sure once we're *inside* the shell, we cd into `cwd`
format!("(cd {cwd}; {}) </dev/null", input.command)
} else {
format!("({}) </dev/null", input.command)
};
let args = vec!["-c".into(), command];
let env = match &working_dir {
Some(dir) => self.project.update(cx, |project, cx| {
project.directory_environment(dir.as_path().into(), cx)
}),
None => Task::ready(None).shared(),
};
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
cx.spawn(async move |cx| {
authorize.await?;
let env = cx.spawn(async move |_| {
let mut env = env.await.unwrap_or_default();
if cfg!(unix) {
env.insert("PAGER".into(), "cat".into());
}
env
});
let terminal = self
.environment
.create_terminal(
input.command.clone(),
working_dir,
Some(COMMAND_OUTPUT_LIMIT),
cx,
)
.await?;
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
let terminal_id = terminal.id(cx)?;
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
..Default::default()
});
cx.spawn({
async move |cx| {
authorize.await?;
let exit_status = terminal.wait_for_exit(cx)?.await;
let output = terminal.current_output(cx)?;
let program = program.await;
let env = env.await;
let terminal = self
.project
.update(cx, |project, cx| {
project.create_terminal_task(
task::SpawnInTerminal {
command: Some(program),
args,
cwd: working_dir.clone(),
env,
..Default::default()
},
cx,
)
})?
.await?;
let acp_terminal = cx.new(|cx| {
acp_thread::Terminal::new(
input.command.clone(),
working_dir.clone(),
terminal.clone(),
language_registry,
cx,
)
})?;
event_stream.update_terminal(acp_terminal.clone());
let exit_status = terminal
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
let (content, content_line_count) = terminal.read_with(cx, |terminal, _| {
(terminal.get_content(), terminal.total_lines())
})?;
let (processed_content, finished_with_empty_output) = process_content(
&content,
&input.command,
exit_status.map(portable_pty::ExitStatus::from),
);
acp_terminal
.update(cx, |terminal, cx| {
terminal.finish(
exit_status,
content.len(),
processed_content.len(),
content_line_count,
finished_with_empty_output,
cx,
);
})
.log_err();
Ok(processed_content)
}
Ok(process_content(output, &input.command, exit_status))
})
}
}
fn process_content(
content: &str,
output: acp::TerminalOutputResponse,
command: &str,
exit_status: Option<portable_pty::ExitStatus>,
) -> (String, bool) {
let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT;
let content = if should_truncate {
let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len());
while !content.is_char_boundary(end_ix) {
end_ix -= 1;
}
// Don't truncate mid-line, clear the remainder of the last line
end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);
&content[..end_ix]
} else {
content
};
let content = content.trim();
exit_status: acp::TerminalExitStatus,
) -> String {
let content = output.output.trim();
let is_empty = content.is_empty();
let content = format!("```\n{content}\n```");
let content = if should_truncate {
let content = if output.truncated {
format!(
"Command output too long. The first {} bytes:\n\n{content}",
content.len(),
@@ -229,24 +143,21 @@ fn process_content(
content
};
let content = match exit_status {
Some(exit_status) if exit_status.success() => {
let content = match exit_status.exit_code {
Some(0) => {
if is_empty {
"Command executed successfully.".to_string()
} else {
content
}
}
Some(exit_status) => {
Some(exit_code) => {
if is_empty {
format!(
"Command \"{command}\" failed with exit code {}.",
exit_status.exit_code()
)
format!("Command \"{command}\" failed with exit code {}.", exit_code)
} else {
format!(
"Command \"{command}\" failed with exit code {}.\n\n{content}",
exit_status.exit_code()
exit_code
)
}
}
@@ -257,7 +168,7 @@ fn process_content(
)
}
};
(content, is_empty)
content
}
fn working_dir(
@@ -300,169 +211,3 @@ fn working_dir(
anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees.");
}
}
#[cfg(test)]
mod tests {
use agent_settings::AgentSettings;
use editor::EditorSettings;
use fs::RealFs;
use gpui::{BackgroundExecutor, TestAppContext};
use pretty_assertions::assert_eq;
use serde_json::json;
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::TerminalSettings;
use theme::ThemeSettings;
use util::test::TempTree;
use crate::ThreadEvent;
use super::*;
fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) {
zlog::init_test();
executor.allow_parking();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
ThemeSettings::register(cx);
TerminalSettings::register(cx);
EditorSettings::register(cx);
AgentSettings::register(cx);
});
}
#[gpui::test]
async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) {
if cfg!(windows) {
return;
}
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
let tree = TempTree::new(json!({
"project": {},
}));
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let input = TerminalToolInput {
command: "cat".to_owned(),
cd: tree
.path()
.join("project")
.as_path()
.to_string_lossy()
.to_string(),
};
let (event_stream_tx, mut event_stream_rx) = ToolCallEventStream::test();
let result = cx
.update(|cx| Arc::new(TerminalTool::new(project, cx)).run(input, event_stream_tx, cx));
let auth = event_stream_rx.expect_authorization().await;
auth.response.send(auth.options[0].id.clone()).unwrap();
event_stream_rx.expect_terminal().await;
assert_eq!(result.await.unwrap(), "Command executed successfully.");
}
#[gpui::test]
async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) {
if cfg!(windows) {
return;
}
init_test(&executor, cx);
let fs = Arc::new(RealFs::new(None, executor));
let tree = TempTree::new(json!({
"project": {},
"other-project": {},
}));
let project: Entity<Project> =
Project::test(fs, [tree.path().join("project").as_path()], cx).await;
let check = |input, expected, cx: &mut TestAppContext| {
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let result = cx.update(|cx| {
Arc::new(TerminalTool::new(project.clone(), cx)).run(input, stream_tx, cx)
});
cx.run_until_parked();
let event = stream_rx.try_next();
if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event {
auth.response.send(auth.options[0].id.clone()).unwrap();
}
cx.spawn(async move |_| {
let output = result.await;
assert_eq!(output.ok(), expected);
})
};
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("project").display()
)),
cx,
)
.await;
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
},
None, // other-project is a dir, but *not* a worktree (yet)
cx,
)
.await;
// Absolute path above the worktree root
check(
TerminalToolInput {
command: "pwd".into(),
cd: tree.path().to_string_lossy().into(),
},
None,
cx,
)
.await;
project
.update(cx, |project, cx| {
project.create_worktree(tree.path().join("other-project"), true, cx)
})
.await
.unwrap();
check(
TerminalToolInput {
command: "pwd".into(),
cd: "other-project".into(),
},
Some(format!(
"```\n{}\n```",
tree.path().join("other-project").display()
)),
cx,
)
.await;
check(
TerminalToolInput {
command: "pwd".into(),
cd: ".".into(),
},
None,
cx,
)
.await;
}
}

View File

@@ -29,7 +29,11 @@ impl AgentTool for ThinkingTool {
acp::ToolKind::Think
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"Thinking".into()
}

View File

@@ -48,7 +48,11 @@ impl AgentTool for WebSearchTool {
acp::ToolKind::Fetch
}
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
"Searching the Web".into()
}

View File

@@ -23,39 +23,31 @@ action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
client = { workspace = true, optional = true }
client.workspace = true
collections.workspace = true
context_server.workspace = true
env_logger = { workspace = true, optional = true }
fs.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio = { workspace = true, optional = true }
http_client.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true
rand.workspace = true
reqwest_client = { workspace = true, optional = true }
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
strum.workspace = true
task.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]

View File

@@ -1,17 +1,17 @@
use crate::AgentServerCommand;
use acp_thread::AgentConnection;
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use agent_settings::AgentSettings;
use anyhow::anyhow;
use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::channel::oneshot;
use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use settings::Settings as _;
use util::ResultExt as _;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error;
@@ -30,7 +30,12 @@ pub struct AcpConnection {
connection: Rc<acp::ClientSideConnection>,
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
prompt_capabilities: acp::PromptCapabilities,
agent_capabilities: acp::AgentCapabilities,
default_mode: Option<acp::SessionModeId>,
root_dir: PathBuf,
// NB: Don't move this into the wait_task, since we need to ensure the process is
// killed on drop (setting kill_on_drop on the command seems to not always work).
child: smol::process::Child,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
@@ -39,15 +44,26 @@ pub struct AcpConnection {
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
}
pub async fn connect(
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
let conn = AcpConnection::stdio(
server_name,
command.clone(),
root_dir,
default_mode,
is_remote,
cx,
)
.await?;
Ok(Rc::new(conn) as _)
}
@@ -58,17 +74,21 @@ impl AcpConnection {
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(command.path)
let mut child = util::command::new_smol_command(command.path);
child
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()?;
.stderr(std::process::Stdio::piped());
if !is_remote {
child.current_dir(root_dir);
}
let mut child = child.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
@@ -104,8 +124,9 @@ impl AcpConnection {
let wait_task = cx.spawn({
let sessions = sessions.clone();
let status_fut = child.status();
async move |cx| {
let status = child.status().await?;
let status = status_fut.await?;
for session in sessions.borrow().values() {
session
@@ -147,18 +168,32 @@ impl AcpConnection {
Ok(Self {
auth_methods: response.auth_methods,
root_dir: root_dir.to_owned(),
connection,
server_name,
sessions,
prompt_capabilities: response.agent_capabilities.prompt_capabilities,
agent_capabilities: response.agent_capabilities,
default_mode,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
child,
})
}
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
&self.prompt_capabilities
&self.agent_capabilities.prompt_capabilities
}
pub fn root_dir(&self) -> &Path {
&self.root_dir
}
}
impl Drop for AcpConnection {
fn drop(&mut self) {
// See the comment on the child field.
self.child.kill().log_err();
}
}
@@ -169,33 +204,42 @@ impl AgentConnection for AcpConnection {
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let name = self.server_name.clone();
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let default_mode = self.default_mode.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
let mcp_servers = if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
})
})
.collect();
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
cx.spawn(async move |cx| {
let response = conn
@@ -215,6 +259,53 @@ impl AgentConnection for AcpConnection {
}
})?;
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
if let Some(default_mode) = default_mode {
if let Some(modes) = modes.as_ref() {
let mut modes_ref = modes.borrow_mut();
let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode);
if has_mode {
let initial_mode_id = modes_ref.current_mode_id.clone();
cx.spawn({
let default_mode = default_mode.clone();
let session_id = response.session_id.clone();
let modes = modes.clone();
async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id: default_mode,
})
.await.log_err();
if result.is_none() {
modes.borrow_mut().current_mode_id = initial_mode_id;
}
}
}).detach();
modes_ref.current_mode_id = default_mode;
} else {
let available_modes = modes_ref
.available_modes
.iter()
.map(|mode| format!("- `{}`: {}", mode.id, mode.name))
.collect::<Vec<_>>()
.join("\n");
log::warn!(
"`{default_mode}` is not valid {name} mode. Available options:\n{available_modes}",
);
}
} else {
log::warn!(
"`{name}` does not support modes, but `default_mode` was set in settings.",
);
}
}
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {
@@ -225,7 +316,7 @@ impl AgentConnection for AcpConnection {
action_log,
session_id.clone(),
// ACP doesn't currently support per-session prompt capabilities or changing capabilities dynamically.
watch::Receiver::constant(self.prompt_capabilities),
watch::Receiver::constant(self.agent_capabilities.prompt_capabilities),
cx,
)
})?;
@@ -233,6 +324,7 @@ impl AgentConnection for AcpConnection {
let session = AcpSession {
thread: thread.downgrade(),
suppress_abort_err: false,
session_modes: modes
};
sessions.borrow_mut().insert(session_id, session);
@@ -329,21 +421,26 @@ impl AgentConnection for AcpConnection {
.detach();
}
fn list_commands(&self, session_id: &acp::SessionId, cx: &mut App) -> Task<Result<acp::ListCommandsResponse>> {
let conn = self.connection.clone();
let session_id = session_id.clone();
cx.foreground_executor().spawn(async move {
conn.list_commands(acp::ListCommandsRequest { session_id }).await
.map_err(Into::into)
})
}
fn session_modes(
&self,
session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionModes>> {
let sessions = self.sessions.clone();
let sessions_ref = sessions.borrow();
let Some(session) = sessions_ref.get(session_id) else {
return None;
};
fn run_command(&self, request: acp::RunCommandRequest, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
conn.run_command(request).await
.map_err(Into::into)
})
if let Some(modes) = session.session_modes.as_ref() {
Some(Rc::new(AcpSessionModes {
connection: self.connection.clone(),
session_id: session_id.clone(),
state: modes.clone(),
}) as _)
} else {
None
}
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
@@ -351,6 +448,50 @@ impl AgentConnection for AcpConnection {
}
}
struct AcpSessionModes {
session_id: acp::SessionId,
connection: Rc<acp::ClientSideConnection>,
state: Rc<RefCell<acp::SessionModeState>>,
}
impl acp_thread::AgentSessionModes for AcpSessionModes {
fn current_mode(&self) -> acp::SessionModeId {
self.state.borrow().current_mode_id.clone()
}
fn all_modes(&self) -> Vec<acp::SessionMode> {
self.state.borrow().available_modes.clone()
}
fn set_mode(&self, mode_id: acp::SessionModeId, cx: &mut App) -> Task<Result<()>> {
let connection = self.connection.clone();
let session_id = self.session_id.clone();
let old_mode_id;
{
let mut state = self.state.borrow_mut();
old_mode_id = state.current_mode_id.clone();
state.current_mode_id = mode_id.clone();
};
let state = self.state.clone();
cx.foreground_executor().spawn(async move {
let result = connection
.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id,
})
.await;
if result.is_err() {
state.borrow_mut().current_mode_id = old_mode_id;
}
result?;
Ok(())
})
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
@@ -361,45 +502,29 @@ impl acp::Client for ClientDelegate {
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let cx = &mut self.cx.clone();
// If always_allow_tool_actions is enabled, then auto-choose the first "Allow" button
if AgentSettings::try_read_global(cx, |settings| settings.always_allow_tool_actions)
.unwrap_or(false)
let respect_always_allow_setting;
let thread;
{
// Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
// some tools would (incorrectly) continue to auto-accept.
if let Some(allow_once_option) = arguments.options.iter().find_map(|option| {
if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
Some(option.id.clone())
} else {
None
}
}) {
return Ok(acp::RequestPermissionResponse {
outcome: acp::RequestPermissionOutcome::Selected {
option_id: allow_once_option,
},
});
}
let sessions_ref = self.sessions.borrow();
let session = sessions_ref
.get(&arguments.session_id)
.context("Failed to get session")?;
respect_always_allow_setting = session.session_modes.is_none();
thread = session.thread.clone();
}
let rx = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})?;
let cx = &mut self.cx.clone();
let result = rx?.await;
let task = thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
arguments.tool_call,
arguments.options,
respect_always_allow_setting,
cx,
)
})??;
let outcome = match result {
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
};
let outcome = task.await;
Ok(acp::RequestPermissionResponse { outcome })
}
@@ -410,11 +535,7 @@ impl acp::Client for ClientDelegate {
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.session_thread(&arguments.session_id)?
.update(cx, |thread, cx| {
thread.write_text_file(arguments.path, arguments.content, cx)
})?;
@@ -428,16 +549,12 @@ impl acp::Client for ClientDelegate {
&self,
arguments: acp::ReadTextFileRequest,
) -> Result<acp::ReadTextFileResponse, acp::Error> {
let cx = &mut self.cx.clone();
let task = self
.sessions
.borrow()
.get(&arguments.session_id)
.context("Failed to get session")?
.thread
.update(cx, |thread, cx| {
let task = self.session_thread(&arguments.session_id)?.update(
&mut self.cx.clone(),
|thread, cx| {
thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
})?;
},
)?;
let content = task.await?;
@@ -448,16 +565,106 @@ impl acp::Client for ClientDelegate {
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
let cx = &mut self.cx.clone();
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
session.thread.update(cx, |thread, cx| {
if let acp::SessionUpdate::CurrentModeUpdate { current_mode_id } = &notification.update {
if let Some(session_modes) = &session.session_modes {
session_modes.borrow_mut().current_mode_id = current_mode_id.clone();
} else {
log::error!(
"Got a `CurrentModeUpdate` notification, but they agent didn't specify `modes` during setting setup."
);
}
}
session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}
async fn create_terminal(
&self,
args: acp::CreateTerminalRequest,
) -> Result<acp::CreateTerminalResponse, acp::Error> {
let terminal = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.create_terminal(
args.command,
args.args,
args.env,
args.cwd,
args.output_byte_limit,
cx,
)
})?
.await?;
Ok(
terminal.read_with(&self.cx, |terminal, _| acp::CreateTerminalResponse {
terminal_id: terminal.id().clone(),
})?,
)
}
async fn kill_terminal(&self, args: acp::KillTerminalRequest) -> Result<(), acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.kill_terminal(args.terminal_id, cx)
})??;
Ok(())
}
async fn release_terminal(&self, args: acp::ReleaseTerminalRequest) -> Result<(), acp::Error> {
self.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.release_terminal(args.terminal_id, cx)
})??;
Ok(())
}
async fn terminal_output(
&self,
args: acp::TerminalOutputRequest,
) -> Result<acp::TerminalOutputResponse, acp::Error> {
self.session_thread(&args.session_id)?
.read_with(&mut self.cx.clone(), |thread, cx| {
let out = thread
.terminal(args.terminal_id)?
.read(cx)
.current_output(cx);
Ok(out)
})?
}
async fn wait_for_terminal_exit(
&self,
args: acp::WaitForTerminalExitRequest,
) -> Result<acp::WaitForTerminalExitResponse, acp::Error> {
let exit_status = self
.session_thread(&args.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit())
})??
.await;
Ok(acp::WaitForTerminalExitResponse { exit_status })
}
}
impl ClientDelegate {
fn session_thread(&self, session_id: &acp::SessionId) -> Result<WeakEntity<AcpThread>> {
let sessions = self.sessions.borrow();
sessions
.get(session_id)
.context("Failed to get session")
.map(|session| session.thread.clone())
}
}

View File

@@ -2,152 +2,76 @@ mod acp;
mod claude;
mod custom;
mod gemini;
mod settings;
#[cfg(any(test, feature = "test-support"))]
pub mod e2e_tests;
pub use claude::*;
use client::ProxySettings;
use collections::HashMap;
pub use custom::*;
use fs::Fs;
pub use gemini::*;
pub use settings::*;
use http_client::read_no_proxy_from_env;
use project::agent_server_store::AgentServerStore;
use acp_thread::AgentConnection;
use acp_thread::LoadError;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use collections::HashMap;
use gpui::AppContext as _;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use node_runtime::VersionStrategy;
use gpui::{App, AppContext, Entity, SharedString, Task};
use project::Project;
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::str::FromStr as _;
use std::{
any::Any,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::ResultExt as _;
use settings::SettingsStore;
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
pub fn init(cx: &mut App) {
settings::init(cx);
}
pub use acp::AcpConnection;
pub struct AgentServerDelegate {
store: Entity<AgentServerStore>,
project: Entity<Project>,
status_tx: watch::Sender<SharedString>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_available: Option<watch::Sender<Option<String>>>,
}
impl AgentServerDelegate {
pub fn new(project: Entity<Project>, status_tx: watch::Sender<SharedString>) -> Self {
Self { project, status_tx }
pub fn new(
store: Entity<AgentServerStore>,
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_tx: Option<watch::Sender<Option<String>>>,
) -> Self {
Self {
store,
project,
status_tx,
new_version_available: new_version_tx,
}
}
pub fn project(&self) -> &Entity<Project> {
&self.project
}
fn get_or_npm_install_builtin_agent(
self,
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
settings: Option<BuiltinAgentServerSettings>,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
if let Some(settings) = &settings
&& let Some(command) = settings.clone().custom_command()
{
return Task::ready(Ok(command));
}
let project = self.project;
let fs = project.read(cx).fs().clone();
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
return Task::ready(Err(anyhow!("Missing node runtime")));
};
let mut status_tx = self.status_tx;
cx.spawn(async move |cx| {
if let Some(settings) = settings && !settings.ignore_system_version.unwrap_or(true) {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand { path: bin, args: Vec::new(), env: Default::default() })
}
}
cx.background_spawn(async move {
let node_path = node_runtime.binary_path().await?;
let dir = paths::data_dir().join("external_agents").join(binary_name.as_str());
fs.create_dir(&dir).await?;
let local_executable_path = dir.join(entrypoint_path);
let command = AgentServerCommand {
path: node_path,
args: vec![local_executable_path.to_string_lossy().to_string()],
env: Default::default(),
};
let installed_version = node_runtime
.npm_package_installed_version(&dir, &package_name)
.await?
.filter(|version| {
Version::from_str(&version)
.is_ok_and(|version| Some(version) >= minimum_version)
});
status_tx.send("Checking for latest version…".into())?;
let latest_version = match node_runtime.npm_package_latest_version(&package_name).await
{
Ok(latest_version) => latest_version,
Err(e) => {
if let Some(installed_version) = installed_version {
log::error!("{e}");
log::warn!("failed to fetch latest version of {package_name}, falling back to cached version {installed_version}");
return Ok(command);
} else {
bail!(e);
}
}
};
let should_install = node_runtime
.should_install_npm_package(
&package_name,
&local_executable_path,
&dir,
VersionStrategy::Latest(&latest_version),
)
.await;
if should_install {
status_tx.send("Installing latest version…".into())?;
node_runtime
.npm_install_packages(&dir, &[(&package_name, &latest_version)])
.await?;
}
Ok(command)
}).await.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
})
}
}
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
None
}
fn set_default_mode(
&self,
_mode_id: Option<agent_client_protocol::SessionModeId>,
_fs: Arc<dyn Fs>,
_cx: &mut App,
) {
}
fn connect(
&self,
root_dir: &Path,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>>;
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -158,119 +82,24 @@ impl dyn AgentServer {
}
}
impl std::fmt::Debug for AgentServerCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let filtered_env = self.env.as_ref().map(|env| {
env.iter()
.map(|(k, v)| {
(
k,
if util::redact::should_redact(k) {
"[REDACTED]"
} else {
v
},
)
})
.collect::<Vec<_>>()
});
/// Load the default proxy environment variables to pass through to the agent
pub fn load_proxy_env(cx: &mut App) -> HashMap<String, String> {
let proxy_url = cx
.read_global(|settings: &SettingsStore, _| settings.get::<ProxySettings>(None).proxy_url());
let mut env = HashMap::default();
f.debug_struct("AgentServerCommand")
.field("path", &self.path)
.field("args", &self.args)
.field("env", &filtered_env)
.finish()
}
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
pub path: PathBuf,
#[serde(default)]
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
}
impl AgentServerCommand {
pub async fn resolve(
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
settings: Option<BuiltinAgentServerSettings>,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<Self> {
if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
Some(command)
if let Some(proxy_url) = &proxy_url {
let env_var = if proxy_url.scheme() == "https" {
"HTTPS_PROXY"
} else {
match find_bin_in_path(path_bin_name.into(), project, cx).await {
Some(path) => Some(Self {
path,
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
env: None,
}),
None => fallback_path.and_then(|path| {
if path.exists() {
Some(Self {
path: path.to_path_buf(),
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
env: None,
})
} else {
None
}
}),
}
}
"HTTP_PROXY"
};
env.insert(env_var.to_owned(), proxy_url.to_string());
}
}
async fn find_bin_in_path(
bin_name: SharedString,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
let (env_task, root_dir) = project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next();
match worktree {
Some(worktree) => {
let env_task = project.environment().update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
});
let path = worktree.read(cx).abs_path();
(env_task, path)
}
None => {
let path: Arc<Path> = paths::home_dir().as_path().into();
let env_task = project.environment().update(cx, |env, cx| {
env.get_directory_environment(path.clone(), cx)
});
(env_task, path)
}
}
})
.log_err()?;
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name.as_str())
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {
return None;
}
which_result.log_err()
})
.await
if let Some(no_proxy) = read_no_proxy_from_env() {
env.insert("NO_PROXY".to_owned(), no_proxy);
}
env
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,178 +0,0 @@
use acp_thread::AcpThread;
use anyhow::Result;
use context_server::{
listener::{McpServerTool, ToolResponse},
types::{ToolAnnotations, ToolResponseContent},
};
use gpui::{AsyncApp, WeakEntity};
use language::unified_diff;
use util::markdown::MarkdownCodeBlock;
use crate::tools::EditToolParams;
#[derive(Clone)]
pub struct EditTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl EditTool {
pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { thread_rx }
}
}
impl McpServerTool for EditTool {
type Input = EditToolParams;
type Output = ();
const NAME: &'static str = "Edit";
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Edit file".to_string()),
read_only_hint: Some(false),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: Some(false),
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
})?
.await?;
let (new_content, diff) = cx
.background_executor()
.spawn(async move {
let new_content = content.replace(&input.old_text, &input.new_text);
if new_content == content {
return Err(anyhow::anyhow!("Failed to find `old_text`",));
}
let diff = unified_diff(&content, &new_content);
Ok((new_content, diff))
})
.await?;
thread
.update(cx, |thread, cx| {
thread.write_text_file(input.abs_path, new_content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: MarkdownCodeBlock {
tag: "diff",
text: diff.as_str().trim_end_matches('\n'),
}
.to_string(),
}],
structured_content: (),
})
}
}
#[cfg(test)]
mod tests {
use std::rc::Rc;
use acp_thread::{AgentConnection, StubAgentConnection};
use gpui::{Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use util::path;
use super::*;
#[gpui::test]
async fn old_text_not_found(cx: &mut TestAppContext) {
let (_thread, tool) = init_test(cx).await;
let result = tool
.run(
EditToolParams {
abs_path: path!("/root/file.txt").into(),
old_text: "hi".into(),
new_text: "bye".into(),
},
&mut cx.to_async(),
)
.await;
assert_eq!(result.unwrap_err().to_string(), "Failed to find `old_text`");
}
#[gpui::test]
async fn found_and_replaced(cx: &mut TestAppContext) {
let (_thread, tool) = init_test(cx).await;
let result = tool
.run(
EditToolParams {
abs_path: path!("/root/file.txt").into(),
old_text: "hello".into(),
new_text: "hi".into(),
},
&mut cx.to_async(),
)
.await;
assert_eq!(
result.unwrap().content[0].text().unwrap(),
indoc! {
r"
```diff
@@ -1,1 +1,1 @@
-hello
+hi
```
"
}
);
}
async fn init_test(cx: &mut TestAppContext) -> (Entity<AcpThread>, EditTool) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
});
let connection = Rc::new(StubAgentConnection::new());
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"file.txt": "hello"
}),
)
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
let thread = cx
.update(|cx| connection.new_thread(project, path!("/test").as_ref(), cx))
.await
.unwrap();
thread_tx.send(thread.downgrade()).unwrap();
(thread, EditTool::new(thread_rx))
}
}

View File

@@ -1,99 +0,0 @@
use std::path::PathBuf;
use std::sync::Arc;
use crate::claude::edit_tool::EditTool;
use crate::claude::permission_tool::PermissionTool;
use crate::claude::read_tool::ReadTool;
use crate::claude::write_tool::WriteTool;
use acp_thread::AcpThread;
#[cfg(not(test))]
use anyhow::Context as _;
use anyhow::Result;
use collections::HashMap;
use context_server::types::{
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
ToolsCapabilities, requests,
};
use gpui::{App, AsyncApp, Task, WeakEntity};
use project::Fs;
use serde::Serialize;
pub struct ClaudeZedMcpServer {
server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
impl ClaudeZedMcpServer {
pub async fn new(
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
fs: Arc<dyn Fs>,
cx: &AsyncApp,
) -> Result<Self> {
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.add_tool(PermissionTool::new(fs.clone(), thread_rx.clone()));
mcp_server.add_tool(ReadTool::new(thread_rx.clone()));
mcp_server.add_tool(EditTool::new(thread_rx.clone()));
mcp_server.add_tool(WriteTool::new(thread_rx.clone()));
Ok(Self { server: mcp_server })
}
pub fn server_config(&self) -> Result<McpServerConfig> {
#[cfg(not(test))]
let zed_path = std::env::current_exe()
.context("finding current executable path for use in mcp_server")?;
#[cfg(test)]
let zed_path = crate::e2e_tests::get_zed_path();
Ok(McpServerConfig {
command: zed_path,
args: vec![
"--nc".into(),
self.server.socket_path().display().to_string(),
],
env: None,
})
}
fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
cx.foreground_executor().spawn(async move {
Ok(InitializeResponse {
protocol_version: ProtocolVersion("2025-06-18".into()),
capabilities: ServerCapabilities {
experimental: None,
logging: None,
completions: None,
prompts: None,
resources: None,
tools: Some(ToolsCapabilities {
list_changed: Some(false),
}),
},
server_info: Implementation {
name: SERVER_NAME.into(),
version: "0.1.0".into(),
},
meta: None,
})
})
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct McpConfig {
pub mcp_servers: HashMap<String, McpServerConfig>,
}
#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct McpServerConfig {
pub command: PathBuf,
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<HashMap<String, String>>,
}

View File

@@ -1,158 +0,0 @@
use std::sync::Arc;
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use context_server::{
listener::{McpServerTool, ToolResponse},
types::ToolResponseContent,
};
use gpui::{AsyncApp, WeakEntity};
use project::Fs;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings as _, update_settings_file};
use util::debug_panic;
use crate::tools::ClaudeTool;
#[derive(Clone)]
pub struct PermissionTool {
fs: Arc<dyn Fs>,
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
/// Request permission for tool calls
#[derive(Deserialize, JsonSchema, Debug)]
pub struct PermissionToolParams {
tool_name: String,
input: serde_json::Value,
tool_use_id: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionToolResponse {
behavior: PermissionToolBehavior,
updated_input: serde_json::Value,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum PermissionToolBehavior {
Allow,
Deny,
}
impl PermissionTool {
pub fn new(fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { fs, thread_rx }
}
}
impl McpServerTool for PermissionTool {
type Input = PermissionToolParams;
type Output = ();
const NAME: &'static str = "Confirmation";
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
if agent_settings::AgentSettings::try_read_global(cx, |settings| {
settings.always_allow_tool_actions
})
.unwrap_or(false)
{
let response = PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
};
return Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
});
}
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
const ALWAYS_ALLOW: &str = "always_allow";
const ALLOW: &str = "allow";
const REJECT: &str = "reject";
let chosen_option = thread
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
claude_tool.as_acp(tool_call_id).into(),
vec![
acp::PermissionOption {
id: acp::PermissionOptionId(ALWAYS_ALLOW.into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
},
acp::PermissionOption {
id: acp::PermissionOptionId(ALLOW.into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
},
acp::PermissionOption {
id: acp::PermissionOptionId(REJECT.into()),
name: "Reject".into(),
kind: acp::PermissionOptionKind::RejectOnce,
},
],
cx,
)
})??
.await?;
let response = match chosen_option.0.as_ref() {
ALWAYS_ALLOW => {
cx.update(|cx| {
update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| {
settings.set_always_allow_tool_actions(true);
});
})?;
PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
}
}
ALLOW => PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
},
REJECT => PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
},
opt => {
debug_panic!("Unexpected option: {}", opt);
PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: input.input,
}
}
};
Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
})
}
}

View File

@@ -1,59 +0,0 @@
use acp_thread::AcpThread;
use anyhow::Result;
use context_server::{
listener::{McpServerTool, ToolResponse},
types::{ToolAnnotations, ToolResponseContent},
};
use gpui::{AsyncApp, WeakEntity};
use crate::tools::ReadToolParams;
#[derive(Clone)]
pub struct ReadTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl ReadTool {
pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { thread_rx }
}
}
impl McpServerTool for ReadTool {
type Input = ReadToolParams;
type Output = ();
const NAME: &'static str = "Read";
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Read file".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: None,
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
let content = thread
.update(cx, |thread, cx| {
thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![ToolResponseContent::Text { text: content }],
structured_content: (),
})
}
}

View File

@@ -1,688 +0,0 @@
use std::path::PathBuf;
use agent_client_protocol as acp;
use itertools::Itertools;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use util::ResultExt;
pub enum ClaudeTool {
Task(Option<TaskToolParams>),
NotebookRead(Option<NotebookReadToolParams>),
NotebookEdit(Option<NotebookEditToolParams>),
Edit(Option<EditToolParams>),
MultiEdit(Option<MultiEditToolParams>),
ReadFile(Option<ReadToolParams>),
Write(Option<WriteToolParams>),
Ls(Option<LsToolParams>),
Glob(Option<GlobToolParams>),
Grep(Option<GrepToolParams>),
Terminal(Option<BashToolParams>),
WebFetch(Option<WebFetchToolParams>),
WebSearch(Option<WebSearchToolParams>),
TodoWrite(Option<TodoWriteToolParams>),
ExitPlanMode(Option<ExitPlanModeToolParams>),
Other {
name: String,
input: serde_json::Value,
},
}
impl ClaudeTool {
pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
match tool_name {
// Known tools
"mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
"mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
"mcp__zed__Write" => Self::Write(serde_json::from_value(input).log_err()),
"MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
"Write" => Self::Write(serde_json::from_value(input).log_err()),
"LS" => Self::Ls(serde_json::from_value(input).log_err()),
"Glob" => Self::Glob(serde_json::from_value(input).log_err()),
"Grep" => Self::Grep(serde_json::from_value(input).log_err()),
"Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
"WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
"WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
"TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
"exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
"Task" => Self::Task(serde_json::from_value(input).log_err()),
"NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
"NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
// Inferred from name
_ => {
let tool_name = tool_name.to_lowercase();
if tool_name.contains("edit") || tool_name.contains("write") {
Self::Edit(None)
} else if tool_name.contains("terminal") {
Self::Terminal(None)
} else {
Self::Other {
name: tool_name,
input,
}
}
}
}
}
pub fn label(&self) -> String {
match &self {
Self::Task(Some(params)) => params.description.clone(),
Self::Task(None) => "Task".into(),
Self::NotebookRead(Some(params)) => {
format!("Read Notebook {}", params.notebook_path.display())
}
Self::NotebookRead(None) => "Read Notebook".into(),
Self::NotebookEdit(Some(params)) => {
format!("Edit Notebook {}", params.notebook_path.display())
}
Self::NotebookEdit(None) => "Edit Notebook".into(),
Self::Terminal(Some(params)) => format!("`{}`", params.command),
Self::Terminal(None) => "Terminal".into(),
Self::ReadFile(_) => "Read File".into(),
Self::Ls(Some(params)) => {
format!("List Directory {}", params.path.display())
}
Self::Ls(None) => "List Directory".into(),
Self::Edit(Some(params)) => {
format!("Edit {}", params.abs_path.display())
}
Self::Edit(None) => "Edit".into(),
Self::MultiEdit(Some(params)) => {
format!("Multi Edit {}", params.file_path.display())
}
Self::MultiEdit(None) => "Multi Edit".into(),
Self::Write(Some(params)) => {
format!("Write {}", params.abs_path.display())
}
Self::Write(None) => "Write".into(),
Self::Glob(Some(params)) => {
format!("Glob `{params}`")
}
Self::Glob(None) => "Glob".into(),
Self::Grep(Some(params)) => format!("`{params}`"),
Self::Grep(None) => "Grep".into(),
Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
Self::WebFetch(None) => "Fetch".into(),
Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
Self::WebSearch(None) => "Web Search".into(),
Self::TodoWrite(Some(params)) => format!(
"Update TODOs: {}",
params.todos.iter().map(|todo| &todo.content).join(", ")
),
Self::TodoWrite(None) => "Update TODOs".into(),
Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
Self::Other { name, .. } => name.clone(),
}
}
pub fn content(&self) -> Vec<acp::ToolCallContent> {
match &self {
Self::Other { input, .. } => vec![
format!(
"```json\n{}```",
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
)
.into(),
],
Self::Task(Some(params)) => vec![params.prompt.clone().into()],
Self::NotebookRead(Some(params)) => {
vec![params.notebook_path.display().to_string().into()]
}
Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
Self::Terminal(Some(params)) => vec![
format!(
"`{}`\n\n{}",
params.command,
params.description.as_deref().unwrap_or_default()
)
.into(),
],
Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
Self::Glob(Some(params)) => vec![params.to_string().into()],
Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
Self::WebSearch(Some(params)) => vec![params.to_string().into()],
Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: Some(params.old_text.clone()),
new_text: params.new_text.clone(),
},
}],
Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: None,
new_text: params.content.clone(),
},
}],
Self::MultiEdit(Some(params)) => {
// todo: show multiple edits in a multibuffer?
params
.edits
.first()
.map(|edit| {
vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: Some(edit.old_string.clone()),
new_text: edit.new_string.clone(),
},
}]
})
.unwrap_or_default()
}
Self::TodoWrite(Some(_)) => {
// These are mapped to plan updates later
vec![]
}
Self::Task(None)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
| Self::Terminal(None)
| Self::ReadFile(None)
| Self::Ls(None)
| Self::Glob(None)
| Self::Grep(None)
| Self::WebFetch(None)
| Self::WebSearch(None)
| Self::TodoWrite(None)
| Self::ExitPlanMode(None)
| Self::Edit(None)
| Self::Write(None)
| Self::MultiEdit(None) => vec![],
}
}
pub fn kind(&self) -> acp::ToolKind {
match self {
Self::Task(_) => acp::ToolKind::Think,
Self::NotebookRead(_) => acp::ToolKind::Read,
Self::NotebookEdit(_) => acp::ToolKind::Edit,
Self::Edit(_) => acp::ToolKind::Edit,
Self::MultiEdit(_) => acp::ToolKind::Edit,
Self::Write(_) => acp::ToolKind::Edit,
Self::ReadFile(_) => acp::ToolKind::Read,
Self::Ls(_) => acp::ToolKind::Search,
Self::Glob(_) => acp::ToolKind::Search,
Self::Grep(_) => acp::ToolKind::Search,
Self::Terminal(_) => acp::ToolKind::Execute,
Self::WebSearch(_) => acp::ToolKind::Search,
Self::WebFetch(_) => acp::ToolKind::Fetch,
Self::TodoWrite(_) => acp::ToolKind::Think,
Self::ExitPlanMode(_) => acp::ToolKind::Think,
Self::Other { .. } => acp::ToolKind::Other,
}
}
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
match &self {
Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: None,
}],
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::Write(Some(WriteToolParams {
abs_path: file_path,
..
})) => {
vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
Self::ReadFile(Some(ReadToolParams {
abs_path, offset, ..
})) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: *offset,
}],
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::Glob(Some(GlobToolParams {
path: Some(path), ..
})) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Grep(Some(GrepToolParams {
path: Some(path), ..
})) => vec![acp::ToolCallLocation {
path: PathBuf::from(path),
line: None,
}],
Self::Task(_)
| Self::NotebookRead(None)
| Self::NotebookEdit(None)
| Self::Edit(None)
| Self::MultiEdit(None)
| Self::Write(None)
| Self::ReadFile(None)
| Self::Ls(None)
| Self::Glob(_)
| Self::Grep(_)
| Self::Terminal(_)
| Self::WebFetch(_)
| Self::WebSearch(_)
| Self::TodoWrite(_)
| Self::ExitPlanMode(_)
| Self::Other { .. } => vec![],
}
}
pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
acp::ToolCall {
id,
kind: self.kind(),
status: acp::ToolCallStatus::InProgress,
title: self.label(),
content: self.content(),
locations: self.locations(),
raw_input: None,
raw_output: None,
}
}
}
/// Edit a file.
///
/// In sessions with mcp__zed__Edit always use it instead of Edit as it will
/// allow the user to conveniently review changes.
///
/// File editing instructions:
/// - The `old_text` param must match existing file content, including indentation.
/// - The `old_text` param must come from the actual file, not an outline.
/// - The `old_text` section must not be empty.
/// - Be minimal with replacements:
/// - For unique lines, include only those lines.
/// - For non-unique lines, include enough context to identify them.
/// - Do not escape quotes, newlines, or other characters.
/// - Only edit the specified file.
#[derive(Deserialize, JsonSchema, Debug)]
pub struct EditToolParams {
/// The absolute path to the file to read.
pub abs_path: PathBuf,
/// The old text to replace (must be unique in the file)
pub old_text: String,
/// The new text.
pub new_text: String,
}
/// Reads the content of the given file in the project.
///
/// Never attempt to read a path that hasn't been previously mentioned.
///
/// In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents.
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ReadToolParams {
/// The absolute path to the file to read.
pub abs_path: PathBuf,
/// Which line to start reading from. Omit to start from the beginning.
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
/// How many lines to read. Omit for the whole file.
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
/// Writes content to the specified file in the project.
///
/// In sessions with mcp__zed__Write always use it instead of Write as it will
/// allow the user to conveniently review changes.
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WriteToolParams {
/// The absolute path of the file to write.
pub abs_path: PathBuf,
/// The full content to write.
pub content: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct BashToolParams {
/// Shell command to execute
pub command: String,
/// 5-10 word description of what command does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Timeout in ms (max 600000ms/10min, default 120000ms)
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct GlobToolParams {
/// Glob pattern like **/*.js or src/**/*.ts
pub pattern: String,
/// Directory to search in (omit for current directory)
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
}
impl std::fmt::Display for GlobToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(path) = &self.path {
write!(f, "{}", path.display())?;
}
write!(f, "{}", self.pattern)
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct LsToolParams {
/// Absolute path to directory
pub path: PathBuf,
/// Array of glob patterns to ignore
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ignore: Vec<String>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct GrepToolParams {
/// Regex pattern to search for
pub pattern: String,
/// File/directory to search (defaults to current directory)
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
/// "content" (shows lines), "files_with_matches" (default), "count"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_mode: Option<GrepOutputMode>,
/// Filter files with glob pattern like "*.js"
#[serde(skip_serializing_if = "Option::is_none")]
pub glob: Option<String>,
/// File type filter like "js", "py", "rust"
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub file_type: Option<String>,
/// Case insensitive search
#[serde(rename = "-i", default, skip_serializing_if = "is_false")]
pub case_insensitive: bool,
/// Show line numbers (content mode only)
#[serde(rename = "-n", default, skip_serializing_if = "is_false")]
pub line_numbers: bool,
/// Lines after match (content mode only)
#[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
pub after_context: Option<u32>,
/// Lines before match (content mode only)
#[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
pub before_context: Option<u32>,
/// Lines before and after match (content mode only)
#[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
pub context: Option<u32>,
/// Enable multiline/cross-line matching
#[serde(default, skip_serializing_if = "is_false")]
pub multiline: bool,
/// Limit output to first N results
#[serde(skip_serializing_if = "Option::is_none")]
pub head_limit: Option<u32>,
}
impl std::fmt::Display for GrepToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "grep")?;
// Boolean flags
if self.case_insensitive {
write!(f, " -i")?;
}
if self.line_numbers {
write!(f, " -n")?;
}
// Context options
if let Some(after) = self.after_context {
write!(f, " -A {}", after)?;
}
if let Some(before) = self.before_context {
write!(f, " -B {}", before)?;
}
if let Some(context) = self.context {
write!(f, " -C {}", context)?;
}
// Output mode
if let Some(mode) = &self.output_mode {
match mode {
GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
GrepOutputMode::Count => write!(f, " -c")?,
GrepOutputMode::Content => {} // Default mode
}
}
// Head limit
if let Some(limit) = self.head_limit {
write!(f, " | head -{}", limit)?;
}
// Glob pattern
if let Some(glob) = &self.glob {
write!(f, " --include=\"{}\"", glob)?;
}
// File type
if let Some(file_type) = &self.file_type {
write!(f, " --type={}", file_type)?;
}
// Multiline
if self.multiline {
write!(f, " -P")?; // Perl-compatible regex for multiline
}
// Pattern (escaped if contains special characters)
write!(f, " \"{}\"", self.pattern)?;
// Path
if let Some(path) = &self.path {
write!(f, " {}", path)?;
}
Ok(())
}
}
#[derive(Default, Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoPriority {
High,
#[default]
Medium,
Low,
}
impl Into<acp::PlanEntryPriority> for TodoPriority {
fn into(self) -> acp::PlanEntryPriority {
match self {
TodoPriority::High => acp::PlanEntryPriority::High,
TodoPriority::Medium => acp::PlanEntryPriority::Medium,
TodoPriority::Low => acp::PlanEntryPriority::Low,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum TodoStatus {
Pending,
InProgress,
Completed,
}
impl Into<acp::PlanEntryStatus> for TodoStatus {
fn into(self) -> acp::PlanEntryStatus {
match self {
TodoStatus::Pending => acp::PlanEntryStatus::Pending,
TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
TodoStatus::Completed => acp::PlanEntryStatus::Completed,
}
}
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct Todo {
/// Task description
pub content: String,
/// Current status of the todo
pub status: TodoStatus,
/// Priority level of the todo
#[serde(default)]
pub priority: TodoPriority,
}
impl Into<acp::PlanEntry> for Todo {
fn into(self) -> acp::PlanEntry {
acp::PlanEntry {
content: self.content,
priority: self.priority.into(),
status: self.status.into(),
}
}
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TodoWriteToolParams {
pub todos: Vec<Todo>,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct ExitPlanModeToolParams {
/// Implementation plan in markdown format
pub plan: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct TaskToolParams {
/// Short 3-5 word description of task
pub description: String,
/// Detailed task for agent to perform
pub prompt: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct NotebookReadToolParams {
/// Absolute path to .ipynb file
pub notebook_path: PathBuf,
/// Specific cell ID to read
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_id: Option<String>,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum CellType {
Code,
Markdown,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum EditMode {
Replace,
Insert,
Delete,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct NotebookEditToolParams {
/// Absolute path to .ipynb file
pub notebook_path: PathBuf,
/// New cell content
pub new_source: String,
/// Cell ID to edit
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_id: Option<String>,
/// Type of cell (code or markdown)
#[serde(skip_serializing_if = "Option::is_none")]
pub cell_type: Option<CellType>,
/// Edit operation mode
#[serde(skip_serializing_if = "Option::is_none")]
pub edit_mode: Option<EditMode>,
}
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
pub struct MultiEditItem {
/// The text to search for and replace
pub old_string: String,
/// The replacement text
pub new_string: String,
/// Whether to replace all occurrences or just the first
#[serde(default, skip_serializing_if = "is_false")]
pub replace_all: bool,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct MultiEditToolParams {
/// Absolute path to file
pub file_path: PathBuf,
/// List of edits to apply
pub edits: Vec<MultiEditItem>,
}
fn is_false(v: &bool) -> bool {
!*v
}
#[derive(Deserialize, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GrepOutputMode {
Content,
FilesWithMatches,
Count,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WebFetchToolParams {
/// Valid URL to fetch
#[serde(rename = "url")]
pub url: String,
/// What to extract from content
pub prompt: String,
}
#[derive(Deserialize, JsonSchema, Debug)]
pub struct WebSearchToolParams {
/// Search query (min 2 chars)
pub query: String,
/// Only include these domains
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub allowed_domains: Vec<String>,
/// Exclude these domains
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub blocked_domains: Vec<String>,
}
impl std::fmt::Display for WebSearchToolParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.query)?;
if !self.allowed_domains.is_empty() {
write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
}
if !self.blocked_domains.is_empty() {
write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
}
Ok(())
}
}

View File

@@ -1,59 +0,0 @@
use acp_thread::AcpThread;
use anyhow::Result;
use context_server::{
listener::{McpServerTool, ToolResponse},
types::ToolAnnotations,
};
use gpui::{AsyncApp, WeakEntity};
use crate::tools::WriteToolParams;
#[derive(Clone)]
pub struct WriteTool {
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
}
impl WriteTool {
pub fn new(thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
Self { thread_rx }
}
}
impl McpServerTool for WriteTool {
type Input = WriteToolParams;
type Output = ();
const NAME: &'static str = "Write";
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: Some("Write file".to_string()),
read_only_hint: Some(false),
destructive_hint: Some(false),
open_world_hint: Some(false),
idempotent_hint: Some(false),
}
}
async fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
};
thread
.update(cx, |thread, cx| {
thread.write_text_file(input.abs_path, input.content, cx)
})?
.await?;
Ok(ToolResponse {
content: vec![],
structured_content: (),
})
}
}

View File

@@ -1,20 +1,22 @@
use crate::{AgentServerCommand, AgentServerDelegate};
use crate::{AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, SharedString, Task};
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
use std::{path::Path, rc::Rc};
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
use settings::{SettingsStore, update_settings_file};
use std::{path::Path, rc::Rc, sync::Arc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
pub struct CustomAgentServer {
name: SharedString,
command: AgentServerCommand,
}
impl CustomAgentServer {
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
Self { name, command }
pub fn new(name: SharedString) -> Self {
Self { name }
}
}
@@ -31,30 +33,67 @@ impl crate::AgentServer for CustomAgentServer {
IconName::Terminal
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: &Path,
_delegate: AgentServerDelegate,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let mut command = self.command.clone();
let root_dir = root_dir.to_path_buf();
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
// TODO: Remove this once we have Claude properly
cx.spawn(async move |mut cx| {
if let Some(api_key) = cx
.update(AnthropicLanguageModelProvider::api_key)?
.await
.ok()
{
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
}
crate::acp::connect(server_name, command, &root_dir, &mut cx).await
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&ExternalAgentServerName(name.clone()))
.with_context(|| {
format!("Custom agent server `{}` is not registered", name)
})?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
))
})??
.await?;
let connection = crate::acp::connect(
name,
command,
root_dir.as_ref(),
default_mode,
is_remote,
cx,
)
.await?;
Ok((connection, login))
})
}

View File

@@ -4,7 +4,9 @@ use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
use project::{FakeFs, Project};
#[cfg(test)]
use project::agent_server_store::BuiltinAgentServerSettings;
use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -447,7 +449,6 @@ pub use common_e2e_tests;
// Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
use settings::Settings;
env_logger::try_init().ok();
@@ -466,12 +467,18 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
language_model::init(client.clone(), cx);
language_models::init(user_store, client, cx);
agent_settings::init(cx);
crate::settings::init(cx);
AllAgentServersSettings::register(cx);
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
claude: Some(crate::claude::tests::local_command().into()),
AllAgentServersSettings::override_global(
AllAgentServersSettings {
claude: Some(BuiltinAgentServerSettings {
path: Some("claude-code-acp".into()),
args: None,
env: None,
ignore_system_version: None,
default_mode: None,
}),
gemini: Some(crate::gemini::tests::local_command().into()),
custom: collections::HashMap::default(),
},
@@ -490,10 +497,11 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let delegate = AgentServerDelegate::new(project.clone(), watch::channel("".into()).0);
let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
let delegate = AgentServerDelegate::new(store, project.clone(), None, None);
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
let (connection, _) = cx
.update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx))
.await
.unwrap();

View File

@@ -1,21 +1,16 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use settings::SettingsStore;
use crate::AllAgentServersSettings;
use project::agent_server_store::GEMINI_NAME;
#[derive(Clone)]
pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini {
fn telemetry_id(&self) -> &'static str {
"gemini-cli"
@@ -31,94 +26,47 @@ impl AgentServer for Gemini {
fn connect(
&self,
root_dir: &Path,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let server_name = self.name();
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
})?;
let mut command = cx
.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
settings,
Some("0.2.1".parse().unwrap()),
cx,
)
})?
.await?;
command.args.push("--experimental-acp".into());
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
}
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&GEMINI_NAME.into())
.context("Gemini CLI is not registered")?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
))
})??
.await?;
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
match &result {
Ok(connection) => {
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
&& !connection.prompt_capabilities().image
{
let version_output = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output()
.await;
let current_version =
String::from_utf8(version_output?.stdout)?.trim().to_owned();
if !connection.prompt_capabilities().image {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
}
Err(_) => {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) =
futures::future::join(version_fut, help_fut).await;
let current_version = std::str::from_utf8(&version_output?.stdout)?
.trim()
.to_string();
let supported = String::from_utf8(help_output?.stdout)?.contains(ACP_ARG);
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: command.path.to_string_lossy().to_string().into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
}
result
let connection = crate::acp::connect(
name,
command,
root_dir.as_ref(),
default_mode,
is_remote,
cx,
)
.await?;
Ok((connection, login))
})
}
@@ -127,18 +75,11 @@ impl AgentServer for Gemini {
}
}
impl Gemini {
const PACKAGE_NAME: &str = "@google/gemini-cli";
const MINIMUM_VERSION: &str = "0.2.1";
const BINARY_NAME: &str = "gemini";
}
#[cfg(test)]
pub(crate) mod tests {
use project::agent_server_store::AgentServerCommand;
use super::*;
use crate::AgentServerCommand;
use std::path::Path;
crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");

View File

@@ -1,3 +1,4 @@
use agent_client_protocol as acp;
use std::path::PathBuf;
use crate::AgentServerCommand;
@@ -6,13 +7,14 @@ use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)]
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<BuiltinAgentServerSettings>,
@@ -45,6 +47,13 @@ pub struct BuiltinAgentServerSettings {
///
/// Default: true
pub ignore_system_version: Option<bool>,
/// The default mode for new threads.
///
/// Note: Not all agents support modes.
///
/// Default: None
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<acp::SessionModeId>,
}
impl BuiltinAgentServerSettings {
@@ -72,11 +81,16 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
/// The default mode for new threads.
///
/// Note: Not all agents support modes.
///
/// Default: None
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<acp::SessionModeId>,
}
impl settings::Settings for AllAgentServersSettings {
const KEY: Option<&'static str> = Some("agent_servers");
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {

View File

@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
#[settings_key(key = "agent", fallback_key = "assistant")]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -268,6 +269,10 @@ pub struct AgentSettingsContent {
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
/// This setting has no effect on external agents that support permission modes, such as Claude Code.
///
/// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Where to show a popup notification when the agent is waiting for user input.
@@ -352,18 +357,19 @@ impl JsonSchema for LanguageModelProviderSetting {
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
json_schema!({
"enum": [
"anthropic",
"amazon-bedrock",
"google",
"lmstudio",
"ollama",
"openai",
"zed.dev",
"anthropic",
"copilot_chat",
"deepseek",
"openrouter",
"google",
"lmstudio",
"mistral",
"vercel"
"ollama",
"openai",
"openrouter",
"vercel",
"x_ai",
"zed.dev"
]
})
}
@@ -398,10 +404,6 @@ pub struct ContextServerPresetContent {
}
impl Settings for AgentSettings {
const KEY: Option<&'static str> = Some("agent");
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AgentSettingsContent;

View File

@@ -25,6 +25,7 @@ agent_servers.workspace = true
agent_settings.workspace = true
ai_onboarding.workspace = true
anyhow.workspace = true
arrayvec.workspace = true
assistant_context.workspace = true
assistant_slash_command.workspace = true
assistant_slash_commands.workspace = true
@@ -80,6 +81,7 @@ serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
shlex.workspace = true
smol.workspace = true
streaming_diff.workspace = true
task.workspace = true

View File

@@ -1,11 +1,13 @@
mod completion_provider;
mod entry_view_state;
mod message_editor;
mod mode_selector;
mod model_selector;
mod model_selector_popover;
mod thread_history;
mod thread_view;
pub use mode_selector::ModeSelector;
pub use model_selector::AcpModelSelector;
pub use model_selector_popover::AcpModelSelectorPopover;
pub use thread_history::*;

View File

@@ -4,7 +4,7 @@ use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use acp_thread::{AcpThread, MentionUri};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
use agent2::{HistoryEntry, HistoryStore};
use anyhow::Result;
@@ -13,9 +13,10 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{App, Entity, Task, WeakEntity};
use language::{Buffer, CodeLabel, HighlightId};
use lsp::CompletionContext;
use project::lsp_store::CompletionDocumentation;
use project::{
Completion, CompletionIntent, CompletionResponse, Project, ProjectPath, Symbol, WorktreeId,
lsp_store::CompletionDocumentation,
Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
ProjectPath, Symbol, WorktreeId,
};
use prompt_store::PromptStore;
use rope::Point;
@@ -24,7 +25,7 @@ use ui::prelude::*;
use workspace::Workspace;
use crate::AgentPanel;
use crate::acp::message_editor::MessageEditor;
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::context_picker::file_context_picker::{FileMatch, search_files};
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
use crate::context_picker::symbol_context_picker::SymbolMatch;
@@ -33,12 +34,6 @@ use crate::context_picker::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
};
#[derive(Debug)]
enum CompletionType {
Mention(MentionCompletion),
SlashCommand(SlashCommandCompletion),
}
pub(crate) enum Match {
File(FileMatch),
Symbol(SymbolMatch),
@@ -54,69 +49,6 @@ pub struct EntryMatch {
entry: ContextPickerEntry,
}
#[derive(Debug, Clone)]
pub struct SlashCommandCompletion {
pub source_range: Range<usize>,
pub command_name: String,
pub argument: Option<String>,
}
impl SlashCommandCompletion {
fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
let last_slash_start = line.rfind('/')?;
if last_slash_start >= line.len() {
return Some(Self {
source_range: last_slash_start + offset_to_line..last_slash_start + 1 + offset_to_line,
command_name: String::new(),
argument: None,
});
}
// Check if slash is at word boundary (not preceded by alphanumeric)
if last_slash_start > 0
&& line
.chars()
.nth(last_slash_start - 1)
.is_some_and(|c| c.is_alphanumeric())
{
return None;
}
let rest_of_line = &line[last_slash_start + 1..];
let mut command_name = String::new();
let mut argument = None;
let mut parts = rest_of_line.split_whitespace();
let mut end = last_slash_start + 1;
if let Some(cmd_text) = parts.next() {
end += cmd_text.len();
command_name = cmd_text.to_string();
// Check for arguments after command name
match rest_of_line[cmd_text.len()..].find(|c: char| !c.is_whitespace()) {
Some(whitespace_count) => {
if let Some(arg_text) = parts.next() {
argument = Some(arg_text.to_string());
end += whitespace_count + arg_text.len();
}
}
None => {
// Rest of line is entirely whitespace
end += rest_of_line.len() - cmd_text.len();
}
}
}
Some(Self {
source_range: last_slash_start + offset_to_line..end + offset_to_line,
command_name,
argument,
})
}
}
impl Match {
pub fn score(&self) -> f64 {
match self {
@@ -137,7 +69,7 @@ pub struct ContextPickerCompletionProvider {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
thread: Rc<RefCell<Option<WeakEntity<AcpThread>>>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
}
impl ContextPickerCompletionProvider {
@@ -147,6 +79,7 @@ impl ContextPickerCompletionProvider {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
) -> Self {
Self {
message_editor,
@@ -154,15 +87,10 @@ impl ContextPickerCompletionProvider {
history_store,
prompt_store,
prompt_capabilities,
thread: Rc::new(RefCell::new(None)),
available_commands,
}
}
/// Set the ACP thread for slash command support
pub fn set_thread(&self, thread: WeakEntity<AcpThread>) {
*self.thread.borrow_mut() = Some(thread);
}
fn completion_for_entry(
entry: ContextPickerEntry,
source_range: Range<Anchor>,
@@ -446,7 +374,42 @@ impl ContextPickerCompletionProvider {
})
}
fn search(
fn search_slash_commands(
&self,
query: String,
cx: &mut App,
) -> Task<Vec<acp::AvailableCommand>> {
let commands = self.available_commands.borrow().clone();
if commands.is_empty() {
return Task::ready(Vec::new());
}
cx.spawn(async move |cx| {
let candidates = commands
.iter()
.enumerate()
.map(|(id, command)| StringMatchCandidate::new(id, &command.name))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
true,
100,
&Arc::new(AtomicBool::default()),
cx.background_executor().clone(),
)
.await;
matches
.into_iter()
.map(|mat| commands[mat.candidate_id].clone())
.collect()
})
}
fn search_mentions(
&self,
mode: Option<ContextPickerMode>,
query: String,
@@ -722,54 +685,196 @@ impl CompletionProvider for ContextPickerCompletionProvider {
_window: &mut Window,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
// Get the buffer state first
let (line, offset_to_line) = buffer.update(cx, |buffer, _cx| {
let state = buffer.update(cx, |buffer, _cx| {
let position = buffer_position.to_point(buffer);
let line_start = Point::new(position.row, 0);
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
let line = lines.next().unwrap_or("");
(line.to_string(), offset_to_line)
});
// Then check for completions outside of the buffer update
let completion_state = {
// First try mention completion
if let Some(mention) = MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
&line,
let line = lines.next()?;
ContextCompletion::try_parse(
line,
offset_to_line,
) {
Some(CompletionType::Mention(mention))
} else if let Some(thread) = self.thread.borrow().as_ref().cloned() {
// Then try slash command completion (only if thread supports commands)
if let Ok(supports_commands) = thread.read_with(cx, |thread, _| {
thread.supports_custom_commands()
}) {
if supports_commands {
if let Some(slash) = SlashCommandCompletion::try_parse(&line, offset_to_line) {
Some(CompletionType::SlashCommand(slash))
} else {
None
}
} else {
None
}
} else {
None
}
} else {
None
}
};
let Some(completion_type) = completion_state else {
self.prompt_capabilities.get().embedded_context,
)
});
let Some(state) = state else {
return Task::ready(Ok(Vec::new()));
};
match completion_type {
CompletionType::Mention(state) => self.complete_mentions(state, buffer.clone(), buffer_position, cx),
CompletionType::SlashCommand(state) => self.complete_slash_commands(state, buffer.clone(), buffer_position, cx),
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
let project = workspace.read(cx).project().clone();
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range().start)
..snapshot.anchor_after(state.source_range().end);
let editor = self.message_editor.clone();
match state {
ContextCompletion::SlashCommand(SlashCommandCompletion {
command, argument, ..
}) => {
let search_task = self.search_slash_commands(command.unwrap_or_default(), cx);
cx.background_spawn(async move {
let completions = search_task
.await
.into_iter()
.map(|command| {
let new_text = if let Some(argument) = argument.as_ref() {
format!("/{} {}", command.name, argument)
} else {
format!("/{} ", command.name)
};
let is_missing_argument = argument.is_none() && command.input.is_some();
Completion {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(command.name.to_string(), None),
documentation: Some(CompletionDocumentation::MultiLinePlainText(
command.description.into(),
)),
source: project::CompletionSource::Custom,
icon_path: None,
insert_text_mode: None,
confirm: Some(Arc::new({
let editor = editor.clone();
move |intent, _window, cx| {
if !is_missing_argument {
cx.defer({
let editor = editor.clone();
move |cx| {
editor
.update(cx, |_editor, cx| {
match intent {
CompletionIntent::Complete
| CompletionIntent::CompleteWithInsert
| CompletionIntent::CompleteWithReplace => {
if !is_missing_argument {
cx.emit(MessageEditorEvent::Send);
}
}
CompletionIntent::Compose => {}
}
})
.ok();
}
});
}
is_missing_argument
}
})),
}
})
.collect();
Ok(vec![CompletionResponse {
completions,
display_options: CompletionDisplayOptions {
dynamic_width: true,
},
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
ContextCompletion::Mention(MentionCompletion { mode, argument, .. }) => {
let query = argument.unwrap_or_default();
let search_task =
self.search_mentions(mode, query, Arc::<AtomicBool>::default(), cx);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let completions = cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
Match::File(FileMatch { mat, is_recent }) => {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
Self::completion_for_path(
project_path,
&mat.path_prefix,
is_recent,
mat.is_dir,
source_range.clone(),
editor.clone(),
project.clone(),
cx,
)
}
Match::Symbol(SymbolMatch { symbol, .. }) => {
Self::completion_for_symbol(
symbol,
source_range.clone(),
editor.clone(),
workspace.clone(),
cx,
)
}
Match::Thread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
false,
editor.clone(),
cx,
)),
Match::RecentThread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
true,
editor.clone(),
cx,
)),
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
source_range.clone(),
editor.clone(),
cx,
)),
Match::Fetch(url) => Self::completion_for_fetch(
source_range.clone(),
url,
editor.clone(),
cx,
),
Match::Entry(EntryMatch { entry, .. }) => {
Self::completion_for_entry(
entry,
source_range.clone(),
editor.clone(),
&workspace,
cx,
)
}
})
.collect()
})?;
Ok(vec![CompletionResponse {
completions,
display_options: CompletionDisplayOptions {
dynamic_width: true,
},
// Since this does its own filtering (see `filter_completions()` returns false),
// there is no benefit to computing whether this set of completions is incomplete.
is_incomplete: true,
}])
})
}
}
}
@@ -788,35 +893,16 @@ impl CompletionProvider for ContextPickerCompletionProvider {
let offset_to_line = buffer.point_to_offset(line_start);
let mut lines = buffer.text_for_range(line_start..position).lines();
if let Some(line) = lines.next() {
// Check for @ mention completions
if let Some(completion) = MentionCompletion::try_parse(
self.prompt_capabilities.get().embedded_context,
ContextCompletion::try_parse(
line,
offset_to_line,
) {
let in_range = completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize;
if in_range {
return true;
}
}
// Check for slash command completions (only if thread supports commands)
if let Some(thread) = self.thread.borrow().as_ref().cloned() {
if let Ok(supports_commands) = thread.read_with(cx, |thread, _| {
thread.supports_custom_commands()
}) {
if supports_commands {
if let Some(completion) = SlashCommandCompletion::try_parse(line, offset_to_line) {
let in_range = completion.source_range.start <= offset_to_line + position.column as usize
&& completion.source_range.end >= offset_to_line + position.column as usize;
return in_range;
}
}
}
}
false
self.prompt_capabilities.get().embedded_context,
)
.map(|completion| {
completion.source_range().start <= offset_to_line + position.column as usize
&& completion.source_range().end >= offset_to_line + position.column as usize
})
.unwrap_or(false)
} else {
false
}
@@ -831,188 +917,6 @@ impl CompletionProvider for ContextPickerCompletionProvider {
}
}
impl ContextPickerCompletionProvider {
fn complete_mentions(
&self,
state: MentionCompletion,
buffer: Entity<Buffer>,
_buffer_position: Anchor,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(Ok(Vec::new()));
};
let project = workspace.read(cx).project().clone();
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
let editor = self.message_editor.clone();
let MentionCompletion { mode, argument, .. } = state;
let query = argument.unwrap_or_else(|| "".to_string());
let search_task = self.search(mode, query, Arc::<AtomicBool>::default(), cx);
cx.spawn(async move |_, cx| {
let matches = search_task.await;
let completions = cx.update(|cx| {
matches
.into_iter()
.filter_map(|mat| match mat {
Match::File(FileMatch { mat, is_recent }) => {
let project_path = ProjectPath {
worktree_id: WorktreeId::from_usize(mat.worktree_id),
path: mat.path.clone(),
};
Self::completion_for_path(
project_path,
&mat.path_prefix,
is_recent,
mat.is_dir,
source_range.clone(),
editor.clone(),
project.clone(),
cx,
)
}
Match::Symbol(SymbolMatch { symbol, .. }) => Self::completion_for_symbol(
symbol,
source_range.clone(),
editor.clone(),
workspace.clone(),
cx,
),
Match::Thread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
false,
editor.clone(),
cx,
)),
Match::RecentThread(thread) => Some(Self::completion_for_thread(
thread,
source_range.clone(),
true,
editor.clone(),
cx,
)),
Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules,
source_range.clone(),
editor.clone(),
cx,
)),
Match::Fetch(url) => Self::completion_for_fetch(
source_range.clone(),
url,
editor.clone(),
cx,
),
Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
entry,
source_range.clone(),
editor.clone(),
&workspace,
cx,
),
})
.collect()
})?;
Ok(vec![CompletionResponse {
completions,
is_incomplete: true,
}])
})
}
fn complete_slash_commands(
&self,
state: SlashCommandCompletion,
buffer: Entity<Buffer>,
_buffer_position: Anchor,
cx: &mut Context<Editor>,
) -> Task<Result<Vec<CompletionResponse>>> {
let Some(thread) = self.thread.borrow().as_ref().cloned() else {
return Task::ready(Ok(Vec::new()));
};
let snapshot = buffer.read(cx).snapshot();
let source_range = snapshot.anchor_before(state.source_range.start)
..snapshot.anchor_after(state.source_range.end);
let command_prefix = state.command_name.clone();
cx.spawn(async move |_, cx| {
// Get session ID and connection from the thread
let (session_id, connection) = thread.read_with(cx, |thread, _| {
(thread.session_id().clone(), thread.connection().clone())
})?;
// Fetch commands from the agent
let commands_task = cx.update(|cx| {
connection.list_commands(&session_id, cx)
})?;
let response = commands_task.await?;
// Filter commands matching the typed prefix
let matching_commands: Vec<_> = response.commands
.into_iter()
.filter(|cmd| {
// Support both prefix matching and fuzzy matching
cmd.name.starts_with(&command_prefix) ||
cmd.name.to_lowercase().contains(&command_prefix.to_lowercase())
})
.collect();
// Convert to project::Completion following existing patterns
let completions: Vec<_> = matching_commands
.into_iter()
.map(|command| {
let new_text = if command.requires_argument {
format!("/{} ", command.name) // Add space for argument
} else {
format!("/{}", command.name)
};
Completion {
replace_range: source_range.clone(),
new_text: new_text.clone(),
label: CodeLabel::plain(command.name.clone(), None),
icon_path: Some(IconName::ZedAssistant.path().into()),
documentation: if !command.description.is_empty() {
Some(CompletionDocumentation::SingleLine(command.description.clone().into()))
} else {
None
},
source: project::CompletionSource::Custom,
insert_text_mode: None,
confirm: Some(Arc::new(move |_, _, _| {
// For now, just insert the text - command execution will be handled later
false
})),
}
})
.collect();
Ok(vec![CompletionResponse {
completions,
is_incomplete: false,
}])
})
}
}
pub(crate) fn search_threads(
query: String,
cancellation_flag: Arc<AtomicBool>,
@@ -1065,7 +969,7 @@ fn confirm_completion_callback(
.clone()
.update(cx, |message_editor, cx| {
message_editor
.confirm_completion(
.confirm_mention_completion(
crease_text,
start,
content_len,
@@ -1081,6 +985,77 @@ fn confirm_completion_callback(
})
}
enum ContextCompletion {
SlashCommand(SlashCommandCompletion),
Mention(MentionCompletion),
}
impl ContextCompletion {
fn source_range(&self) -> Range<usize> {
match self {
Self::SlashCommand(completion) => completion.source_range.clone(),
Self::Mention(completion) => completion.source_range.clone(),
}
}
fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option<Self> {
if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) {
Some(Self::SlashCommand(command))
} else if let Some(mention) =
MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line)
{
Some(Self::Mention(mention))
} else {
None
}
}
}
#[derive(Debug, Default, PartialEq)]
pub struct SlashCommandCompletion {
pub source_range: Range<usize>,
pub command: Option<String>,
pub argument: Option<String>,
}
impl SlashCommandCompletion {
pub fn try_parse(line: &str, offset_to_line: usize) -> Option<Self> {
// If we decide to support commands that are not at the beginning of the prompt, we can remove this check
if !line.starts_with('/') || offset_to_line != 0 {
return None;
}
let (prefix, last_command) = line.rsplit_once('/')?;
if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
|| last_command.starts_with(char::is_whitespace)
{
return None;
}
let mut argument = None;
let mut command = None;
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
if !args.is_empty() {
argument = Some(args.trim_end().to_string());
}
command = Some(command_text.to_string());
} else if !last_command.is_empty() {
command = Some(last_command.to_string());
};
Some(Self {
source_range: prefix.len() + offset_to_line
..line
.rfind(|c: char| !c.is_whitespace())
.unwrap_or_else(|| line.len())
+ 1
+ offset_to_line,
command,
argument,
})
}
}
#[derive(Debug, Default, PartialEq)]
struct MentionCompletion {
source_range: Range<usize>,
@@ -1091,13 +1066,21 @@ struct MentionCompletion {
impl MentionCompletion {
fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
let last_mention_start = line.rfind('@')?;
if last_mention_start >= line.len() {
return Some(Self::default());
// No whitespace immediately after '@'
if line[last_mention_start + 1..]
.chars()
.next()
.is_some_and(|c| c.is_whitespace())
{
return None;
}
// Must be a word boundary before '@'
if last_mention_start > 0
&& line
&& line[..last_mention_start]
.chars()
.nth(last_mention_start - 1)
.last()
.is_some_and(|c| !c.is_whitespace())
{
return None;
@@ -1110,7 +1093,9 @@ impl MentionCompletion {
let mut parts = rest_of_line.split_whitespace();
let mut end = last_mention_start + 1;
if let Some(mode_text) = parts.next() {
// Safe since we check no leading whitespace above
end += mode_text.len();
if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
@@ -1142,11 +1127,77 @@ impl MentionCompletion {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slash_command_completion_parse() {
assert_eq!(
SlashCommandCompletion::try_parse("/", 0),
Some(SlashCommandCompletion {
source_range: 0..1,
command: None,
argument: None,
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help", 0),
Some(SlashCommandCompletion {
source_range: 0..5,
command: Some("help".to_string()),
argument: None,
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help ", 0),
Some(SlashCommandCompletion {
source_range: 0..5,
command: Some("help".to_string()),
argument: None,
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help arg1", 0),
Some(SlashCommandCompletion {
source_range: 0..10,
command: Some("help".to_string()),
argument: Some("arg1".to_string()),
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/help arg1 arg2", 0),
Some(SlashCommandCompletion {
source_range: 0..15,
command: Some("help".to_string()),
argument: Some("arg1 arg2".to_string()),
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
Some(SlashCommandCompletion {
source_range: 0..30,
command: Some("拿不到命令".to_string()),
argument: Some("拿不到命令".to_string()),
})
);
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
}
#[test]
fn test_mention_completion_parse() {
assert_eq!(MentionCompletion::try_parse(true, "Lorem Ipsum", 0), None);
@@ -1237,5 +1288,23 @@ mod tests {
argument: Some("main".to_string()),
})
);
assert_eq!(
MentionCompletion::try_parse(true, "Lorem@symbol", 0),
None,
"Should not parse mention inside word"
);
assert_eq!(
MentionCompletion::try_parse(true, "Lorem @ file", 0),
None,
"Should not parse with a space after @"
);
assert_eq!(
MentionCompletion::try_parse(true, "@ file", 0),
None,
"Should not parse with a space after @ at the start of the line"
);
}
}

View File

@@ -1,13 +1,17 @@
use std::{cell::Cell, ops::Range, rc::Rc};
use std::{
cell::{Cell, RefCell},
ops::Range,
rc::Rc,
};
use acp_thread::{AcpThread, AgentThreadEntry};
use agent_client_protocol::{PromptCapabilities, ToolCallId};
use agent_client_protocol::{self as acp, ToolCallId};
use agent2::HistoryStore;
use collections::HashMap;
use editor::{Editor, EditorMode, MinimapVisibility};
use gpui::{
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
ScrollHandle, TextStyleRefinement, WeakEntity, Window,
ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window,
};
use language::language_settings::SoftWrap;
use project::Project;
@@ -26,8 +30,9 @@ pub struct EntryViewState {
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
}
impl EntryViewState {
@@ -36,8 +41,9 @@ impl EntryViewState {
project: Entity<Project>,
history_store: Entity<HistoryStore>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
prevent_slash_commands: bool,
prompt_capabilities: Rc<Cell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
agent_name: SharedString,
) -> Self {
Self {
workspace,
@@ -45,8 +51,9 @@ impl EntryViewState {
history_store,
prompt_store,
entries: Vec::new(),
prevent_slash_commands,
prompt_capabilities,
available_commands,
agent_name,
}
}
@@ -85,8 +92,9 @@ impl EntryViewState {
self.history_store.clone(),
self.prompt_store.clone(),
self.prompt_capabilities.clone(),
self.available_commands.clone(),
self.agent_name.clone(),
"Edit message @ to include context",
self.prevent_slash_commands,
editor::EditorMode::AutoHeight {
min_lines: 1,
max_lines: None,
@@ -125,22 +133,35 @@ impl EntryViewState {
views
};
let is_tool_call_completed =
matches!(tool_call.status, acp_thread::ToolCallStatus::Completed);
for terminal in terminals {
views.entry(terminal.entity_id()).or_insert_with(|| {
let element = create_terminal(
self.workspace.clone(),
self.project.clone(),
terminal.clone(),
window,
cx,
)
.into_any();
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::NewTerminal(id.clone()),
});
element
});
match views.entry(terminal.entity_id()) {
collections::hash_map::Entry::Vacant(entry) => {
let element = create_terminal(
self.workspace.clone(),
self.project.clone(),
terminal.clone(),
window,
cx,
)
.into_any();
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::NewTerminal(id.clone()),
});
entry.insert(element);
}
collections::hash_map::Entry::Occupied(_entry) => {
if is_tool_call_completed && terminal.read(cx).output().is_none() {
cx.emit(EntryViewEvent {
entry_index: index,
view_event: ViewEvent::TerminalMovedToBackground(id.clone()),
});
}
}
}
}
for diff in diffs {
@@ -186,7 +207,7 @@ impl EntryViewState {
self.entries.drain(range);
}
pub fn settings_changed(&mut self, cx: &mut App) {
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
for entry in self.entries.iter() {
match entry {
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
@@ -217,6 +238,7 @@ pub struct EntryViewEvent {
pub enum ViewEvent {
NewDiff(ToolCallId),
NewTerminal(ToolCallId),
TerminalMovedToBackground(ToolCallId),
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
}
@@ -457,7 +479,8 @@ mod tests {
history_store,
None,
Default::default(),
false,
Default::default(),
"Test Agent".into(),
)
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,230 @@
use acp_thread::AgentSessionModes;
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use fs::Fs;
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use std::{rc::Rc, sync::Arc};
use ui::{
Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector};
pub struct ModeSelector {
connection: Rc<dyn AgentSessionModes>,
agent_server: Rc<dyn AgentServer>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
setting_mode: bool,
}
impl ModeSelector {
pub fn new(
session_modes: Rc<dyn AgentSessionModes>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
) -> Self {
Self {
connection: session_modes,
agent_server,
menu_handle: PopoverMenuHandle::default(),
fs,
setting_mode: false,
focus_handle,
}
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
let all_modes = self.connection.all_modes();
let current_mode = self.connection.current_mode();
let current_index = all_modes
.iter()
.position(|mode| mode.id.0 == current_mode.0)
.unwrap_or(0);
let next_index = (current_index + 1) % all_modes.len();
self.set_mode(all_modes[next_index].id.clone(), cx);
}
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
let task = self.connection.set_mode(mode, cx);
self.setting_mode = true;
cx.notify();
cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
if let Err(err) = task.await {
log::error!("Failed to set session mode: {:?}", err);
}
this.update(cx, |this, cx| {
this.setting_mode = false;
cx.notify();
})
.ok();
})
.detach();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let weak_self = cx.weak_entity();
ContextMenu::build(window, cx, move |mut menu, _window, cx| {
let all_modes = self.connection.all_modes();
let current_mode = self.connection.current_mode();
let default_mode = self.agent_server.default_mode(cx);
for mode in all_modes {
let is_selected = &mode.id == &current_mode;
let is_default = Some(&mode.id) == default_mode.as_ref();
let entry = ContextMenuEntry::new(mode.name.clone())
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
entry.documentation_aside(ui::DocumentationSide::Left, {
let description = description.clone();
move |cx| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(div().pt_0p5().children(ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
)))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
} else {
this.child("to also set as default")
}
})),
)
.into_any_element()
}
})
} else {
entry
};
menu.push_item(entry.handler({
let mode_id = mode.id.clone();
let weak_self = weak_self.clone();
move |window, cx| {
weak_self
.update(cx, |this, cx| {
if window.modifiers().secondary() {
this.agent_server.set_default_mode(
if is_default {
None
} else {
Some(mode_id.clone())
},
this.fs.clone(),
cx,
);
}
this.set_mode(mode_id.clone(), cx);
})
.ok();
}
}));
}
menu.key_context("ModeSelector")
})
}
}
impl Render for ModeSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let current_mode_id = self.connection.current_mode();
let current_mode_name = self
.connection
.all_modes()
.iter()
.find(|mode| mode.id == current_mode_id)
.map(|mode| mode.name.clone())
.unwrap_or_else(|| "Unknown".into());
let this = cx.entity();
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
.label_size(LabelSize::Small)
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.disabled(self.setting_mode);
PopoverMenu::new("mode-selector")
.trigger_with_tooltip(
trigger_button,
Tooltip::element({
let focus_handle = self.focus_handle.clone();
move |window, cx| {
v_flex()
.gap_1()
.child(
h_flex()
.pb_1()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(Label::new("Cycle Through Modes"))
.children(KeyBinding::for_action_in(
&CycleModeSelector,
&focus_handle,
window,
cx,
)),
)
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.children(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)),
)
.into_any()
}
}),
)
.anchor(gpui::Corner::BottomRight)
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
}
}

View File

@@ -192,8 +192,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
}
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.defer_in(window, |picker, window, cx| {
picker.set_query("", window, cx);
});
}
fn render_match(

View File

@@ -5,7 +5,8 @@ use agent_client_protocol as acp;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use ui::{
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*,
ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window,
prelude::*,
};
use zed_actions::agent::ToggleModelSelector;
@@ -36,6 +37,14 @@ impl AcpModelSelectorPopover {
pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
self.menu_handle.toggle(window, cx);
}
pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
self.selector
.read(cx)
.delegate
.active_model()
.map(|model| model.name.clone())
}
}
impl Render for AcpModelSelectorPopover {
@@ -50,15 +59,22 @@ impl Render for AcpModelSelectorPopover {
let focus_handle = self.focus_handle.clone();
let color = if self.menu_handle.is_deployed() {
Color::Accent
} else {
Color::Muted
};
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(model_icon, |this, icon| {
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
})
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
Label::new(model_name)
.color(Color::Muted)
.color(color)
.size(LabelSize::Small)
.ml_0p5(),
)

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