Compare commits

...

188 Commits

Author SHA1 Message Date
Finn Evers
872b2b3510 language: Change signature of initialization_options_schema (#45937)
This makes this take the LSP adapter delegate instead of the binary
itself.

Despite us passing `LanguageServerBinaryOptions` with `allow_download:
false`, extensions would still try to download the binary because it was
never implemented for these to respect that. This would cause us to try
to download all langauge servers provided by extensions when opening a
settings file and/or requesting the JSON schema for that.

This PR fixes this by passing the LSP adapter delegate instead, so the
few language servers which actually want to have the binary for
resolving the initialization options can decide on this by themselves.
With that, we no longer download all language servers for the schema
request

Release Notes:

- N/A
2026-01-01 18:20:53 +00:00
Finn Evers
83966115a8 Update community champions list (#45935)
Updates the community champions list to the latest state, adding some of
our active extension contributors, and sorts the list/removes
duplicates.

Release Notes:

- N/A
2026-01-01 17:47:08 +00:00
Cole Miller
554382a87e git: Rework side-by-side diff to use distinct buffers for the left-hand side (#44838)
This PR reworks the (still feature-gated) side-by-side diff view to use
a different approach to representing the multibuffers on the left- and
right-hand sides.

Previously, these two multibuffers used identical sets of buffers and
excerpts, and were made to behave differently by adding a new knob to
the multibuffer controlling how diffs are displayed. Specifically, the
left-hand side multibuffer would filter out the added range of each hunk
from the excerpts using a new `FilteredInsertedHunk` diff transform, and
the right-hand side would simply not show the deleted sides of expanded
hunks. This approach has some problems:

- Line numbers, and actions that navigate by line number, behaved
incorrectly for the left-hand side.
- Syntax highlighting and other features that use the buffer syntax tree
also behaved incorrectly for the left-hand side.

In this PR, we've switched to using independent buffers to build the
left-hand side. These buffers are constructed using the base texts for
the corresponding diffs, and their lifecycle is managed by `BufferDiff`.
The red "deleted" regions on the left-hand side are represented by
`BufferContent` diff transforms, not `DeletedHunk` transforms. This
means each excerpt on the left represents a contiguous slice of a single
buffer, which fixes the above issues by construction.

The tradeoff with this new approach is that we now have to manually
synchronize excerpt ranges from the right side to the left, which we do
using `BufferDiffSnapshot::row_to_base_text_row`.

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
Co-authored-by: Miguel Raz Guzmán Macedo <miguel@zed.dev>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Cameron <cameron@zed.dev>
2025-12-31 20:02:45 -05:00
Haojian Wu
9677da9a60 docs: Remove reference to non-existing binding for tab switcher: toggle all (#45919)
There is no default binding for `{#kb tab_switcher::ToggleAll}`, so the
doc is rendered as:

<img width="1330" height="227" alt="image"
src="https://github.com/user-attachments/assets/a5ce2efd-69c4-4eb4-a28b-3fdb7825ce34"
/>

Release Notes:

- N/A
2025-12-31 22:59:14 +00:00
Haojian Wu
9e5546edc5 docs: Use inline code in debugger.md (#45920)
Release Notes:

- N/A
2025-12-31 22:56:39 +00:00
Haojian Wu
c7203f7a60 docs: Fix an incorrect code-snippet in multibuffers.md (#45918)
Release Notes:

- N/A
2025-12-31 23:54:18 +01:00
Haojian Wu
c87f12ee42 docs: Update workspace symbol search command in configuring-languages.md (#45906)
Release Notes:

- N/A

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-12-31 23:00:53 +01:00
Haojian Wu
084d01b5f0 docs: Fix project-wide diagnostic command in configuring-languages.md (#45905)
Release Notes:

- N/A

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-12-31 23:00:36 +01:00
Piotr Osiewicz
ed7360206f debugger: Fix breakpoint store RPC handlers not being registered correctly on SSH remotes (#44908)
Closes #36789

Release Notes:

- Fixed setting breakpoints on remotes

---------

Co-authored-by: Zed AI <ai@zed.dev>
2025-12-31 19:40:11 +01:00
Bennet Bo Fenner
9f599466b5 agent_servers: Fix process leaks after terminating ACP server (#45902)
Closes #45211

This ensures that all sub-processes that were launched by the ACP server
are terminated. One scenario where this is easily reproducible:
- Start a new Claude Code ACP session
- Submit a prompt
- While Claude-code is still responding, start a new session
- The `claude-code` subprocess is leaked from the previous session (The
Claude-code SDK runs the Claude-code binary in a sub process)

This PR fixes this by using process groups on Unix. 
It does not fix the process leaks on Windows yet (will follow up with
another PR)

Release Notes:

- Fixed an issue where subprocesses of ACP servers could be leaked after
starting a new session
2025-12-31 11:59:50 +00:00
Nick Cho
dffda91838 Fixes a doc link to the license detection code (#45894)
The existing link was 404-ing as the crate was renamed.

Not attached to any issue.

Release Notes:

- N/A
2025-12-31 10:07:05 +00:00
flou
6b3412232f python: Add support for uv run as the shebang line (#45881)
`uv run` can be used to specify a shebang for a Python script.

See:
https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to-create-an-executable-file

Release Notes:

- Added support for `uv run` in the shebang line to be interpreted as a
Python script
2025-12-31 01:54:05 +01:00
Danilo Leal
963cb2c200 settings_ui: Make font size settings use editable number fields (#45875)
Now that the edit mode in number fields is finally working well, we can
make the UX of editing font sizes much nicer because you can now type
inside the number field :)


https://github.com/user-attachments/assets/8df7c6ee-e82b-4e10-a175-e0ca5f1bab1f

Release Notes:

- settings UI: Improved the UX of editing font size fields as you can
now type the desired value as opposed to just using the
decrement/increment buttons.
2025-12-30 14:51:58 -03:00
Danilo Leal
0c7d639cbd ui: Add fixes to the NumberField edit mode (#45871)
Follow up to https://github.com/zed-industries/zed/pull/45447 and in
preparation to enable the edit mode in number field instances within the
settings UI. This PR fixes the editor in the number field capturing
focus automatically (unnecessary), tab index in the buttons and editor,
and other things.

Release Notes:

- N/A
2025-12-30 14:24:33 -03:00
Danilo Leal
189b5ff3a2 ui: Make docs aside in pickers and context menu render centered to its trigger (#45868)
This has been something we've wanted to do for a long time since docs
aside thus far have been hard-anchored either at the top or bottom of
the container that their trigger is sitting in. This PR introduces the
change so that they're centered with their trigger, regardless of
whether it's on a context menu or picker, by including a canvas element
in both the container and the trigger to calculate where the docs aside
should precisely sit on. Here's the result:


https://github.com/user-attachments/assets/8147ad05-1927-4353-991d-405631de67d0

Note that at the moment, docs aside are only visible through _hovering_,
and ideally, they should be available on both hover and selection
(keyboard nav). But I'll leave that for later.

Release Notes:

- N/A
2025-12-30 13:29:44 -03:00
Finn Evers
a3bf58d393 extension_ci: Improve title formatting (#45870)
Just some minor formatting touchups I noticed after the first rollout

Release Notes:

- N/A
2025-12-30 15:39:02 +00:00
Finn Evers
d844f5db30 extension_ci: Fix version bump not working (#45869)
I changed the runner sizes to a smaller but more recent image yesterday
and broke the version bumping in the process. This PR fixes this by
force installing the needed package.

Release Notes:

- N/A
2025-12-30 15:27:20 +00:00
Jason Lee
fc45f6e042 title_bar: Fix close button hover color on Windows (#45867)
Release Notes:

- Fix close button hover text color on Windows

| Before | After |
| --- | --- |
| <img width="166" height="137" alt="before"
src="https://github.com/user-attachments/assets/fcd225c4-c73f-4e21-8e8d-459395df27ec"
/> |<img width="188" height="156" alt="after"
src="https://github.com/user-attachments/assets/89f5dcba-d677-4eb9-a9b4-170eb9792cb2"
/> |


https://github.com/user-attachments/assets/27e88bf1-eddd-4ec8-a360-6a68eef484e0

And I also improved this file's implementation to merge the
`WindowsCaptionButtonIcon` into `WindowsCaptionButton`.
2025-12-30 15:17:17 +00:00
Piotr Osiewicz
843be766df project search: Make cancellation smoother (#45406)
- **search: Make search cancellation more responsive (again)**
- **Fix project benchmarks build**
- **Less scoping and lifetimes for workers**

Related to #45300

Release Notes:

- Project search will consume less resources immediately after
cancellation.

---------

Co-authored-by: Max Brunsfeld <max@zed.dev>
2025-12-30 15:11:05 +01:00
Danilo Leal
469ad070b0 ui: Add submenus to ContextMenu (#45743)
This PR introduces submenu functionality for Zed's dropdown/context
menus.

<img width="600" height="830" alt="Screenshot 2025-12-27 at 2  03@2x"
src="https://github.com/user-attachments/assets/eadfcd74-07fe-4a1f-be76-11b547c16dc8"
/>

```rs
.submenu("Trigger", |menu, _, _| {
    menu.entry("Item…", None, |_, _| {})
        .entry("Item…", None, |_, _| {})
})
```

Release Notes:

- N/A

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
2025-12-30 07:49:01 -03:00
Kirill Bulatov
392b6184bf Fix worktree trust handling of multiple projects on the same remote host (#45834)
Closes https://github.com/zed-industries/zed/issues/45630

Remote host location alone is not enough to distinguish between remote
worktrees: different remote projects open in different windows will have
the same remote host location and _will_ have the same `WorktreeId`.

Thus, require an associated `WorktreeStore` with all
`WorktreeId`-related trust questions, and store those IDs based on the
store key.

Release Notes:

- Fixed worktree trust handling of multiple projects on the same remote
host
2025-12-29 20:21:59 +00:00
Finn Evers
fc89e19098 extension_ci: Move shared workflows into nested folder (#45828)
This makes the rollout as well as distinguishing this in the future
easier.

Release Notes:

- N/A
2025-12-29 16:44:47 +00:00
Finn Evers
f53b01d5a2 ci: Grant GitHub token more granular permissions (#45825)
Release Notes:

- N/A
2025-12-29 16:16:01 +00:00
Finn Evers
bf1c8819d9 ci: Properly request token for extension repositories (#45824)
Release Notes:

- N/A
2025-12-29 15:41:56 +00:00
Xiaobo Liu
3247264288 ui: Remove stray blank lines in ButtonStyle methods (#45822)
Release Notes:

- N/A
2025-12-29 14:23:49 +00:00
Danilo Leal
6d947b7746 ui: Add a CopyButton component (#45821)
There were several places adding a copy icon button, so thought of
encapsulating the logic to copy a given string into the clipboard (and
other small details like swapping the icon and tooltip if copied) into a
component, making it easier to introduce this sort of functionality in
the future, with fewer lines of code.

All it takes (for the simplest case) is:

```rs
CopyButton::new(your_message)
```

<img width="600" height="714" alt="Screenshot 2025-12-29 at 10  50@2x"
src="https://github.com/user-attachments/assets/e6949863-a056-4855-82d8-e4ffb5d62c90"
/>

Release Notes:

- N/A
2025-12-29 11:01:19 -03:00
Finn Evers
db221ca72d Add workflow to rollout changes to the extension organization (#45579)
This PR adds a workflow that we can utilize to rollout changes to the CI
workflows for the `zed-extensions` organization.

Release Notes:

- N/A
2025-12-29 14:51:17 +01:00
Finn Evers
1d006a8cb0 extension_ci: Specify needed permissions for jobs (#45542)
GitHub flags these as security vulnerabilities. Hence, this PR specifies
the needed permissions for the workflows used in the `zed-extensions`
organization.

Release Notes:

- N/A
2025-12-29 13:19:22 +00:00
Rocky Shi
aaab9f6960 Add a button to copy diagnostic messages from the hover popover to the clipboard (#45625)
Closes https://github.com/zed-industries/zed/issues/45346

Release Notes:

- Added a button to copy diagnostic messages from the hove popover

Screenshot:

<img width="842" height="360" alt="image"
src="https://github.com/user-attachments/assets/9c00fba5-82aa-4179-95b1-afd5c1173889"
/>

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-29 12:31:29 +00:00
Ahmed Hesham Abdelkader
209cf0a48f ui: Make long Callout descriptions scrollable (#45792)
Fixes #43306

Long error messages from LLM providers in the Agent Panel were not
scrollable, making it impossible to read the full error content.

Changes:
- Add max_h_48() and overflow_y_scroll() to description containers
- Add element IDs required for scroll functionality
- Add min_h_0() and overflow_hidden() to parent flex container
- Add component preview example demonstrating scrollable content

Release Notes:

- Fixed long error messages in Agent Panel being unreadable by making
them scrollable
([#43306](https://github.com/zed-industries/zed/issues/43306)).

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-29 12:03:50 +00:00
Murilo Cunha
260691c99c docs: Specify that dev containers are currently previeiw-only (#45816)
Thanks for the cool project and making it open source! Started using Zed
recently and I really enjoy it.

Made a tiny addition to devcontainer docs to specify the version. Wasn't
able to get it to work as shown in the
[docs](https://zed.dev/docs/dev-containers) (should "just work"). The
feature was introduced recently on [PR
44442](https://github.com/zed-industries/zed/pull/44442) and is only
available as of v0.218 (currently still in preview), while I was still
on the latest stable version.

So I thought of opening a small PR 😊 

Thanks again for the awesome project!

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
2025-12-29 11:34:52 +00:00
Danilo Leal
9e88f3f33c agent_ui: Fix issues with mention crease (#45683)
This PR introduces the `MentionCrease` component, aimed at solving two
issues with mention creases in the agent panel:
- Previously, the mention crease was using a button with a regular size,
which is bigger than the default buffer font line height. That made the
crease look clipped and also overlapping with one another when in a
multiple line scenario where the creases would be on top of each other.
`MentionCrease` uses the window line height value to set the button
height, with a small one pixel vertical padding just for a bit of
spacing.
- Previously, given the crease used a `Label`, its font size wouldn't
scale if you changed the `agent_buffer_font_size` setting. Now,
`MentionCrease` uses that font size value, which makes regular text and
its text grow together as you'd expect.

Release Notes:

- agent: Fix a bug where mention creases didn't scale with
`agent_buffer_font_size` and got clipped/jumbled when rendered one above
the other.
2025-12-29 08:27:43 -03:00
Gabe Shahbazian
2cad6c8ef1 svg_preview: Detect SVG in single-file mode by checking file name (#45747)
Release Notes:

- Use the files name for "is svg" checks so SVG previews and the toolbar
button work in single-file mode.
2025-12-27 23:29:36 +00:00
Ben Brandt
bc24ffe863 acp: Beta support for Session Config Options (#45751)
Adds beta support for the ACP draft feature of Session Config Options:
https://agentclientprotocol.com/rfds/session-config-options

Release Notes:

- N/A
2025-12-27 22:10:37 +00:00
Kirill Bulatov
1e4a970ae2 Bump glsl to 0.2.0 (#45744)
Includes https://github.com/zed-industries/zed/pull/45727

Release Notes:

- N/A
2025-12-27 19:09:52 +00:00
Thinkseal
3e656a0911 Add colorized brackets support for GLSL (#45727)
Closes #45674

before:
<img width="482" height="271" alt="Screenshot 2025-12-26 at 21 05 50"
src="https://github.com/user-attachments/assets/d1bba3e1-04f3-4b8d-a187-8da80cee7e22"
/>
after
<img width="481" height="244" alt="Screenshot 2025-12-26 at 21 06 23"
src="https://github.com/user-attachments/assets/4778c779-7082-4701-821e-b825b05b4097"
/>


Release Notes:

- Fixes colorized brackets for the GLSL extension

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
2025-12-27 18:54:23 +00:00
Finn Evers
57ea23d161 editor: Fix active line number regressions with relative counting (#45741)
Follow-up to https://github.com/zed-industries/zed/pull/45164 which
caused the active line number to always be `0` instead of the actual
current line number. No release notes since its only on nightly

Release Notes:

- N/A
2025-12-27 16:26:08 +00:00
Kirill Bulatov
a50c5b2c10 Fix Zed OOM-ing when macOS file descriptors become invalid (#45669)
Closes https://github.com/zed-industries/zed/issues/42845

Repro steps:
https://github.com/zed-industries/zed/issues/42845#issuecomment-3687413958
Initial investigation and Zed memory trace:
https://github.com/zed-industries/zed/issues/42845#issuecomment-3687877977

The PR consists of 2 commits:
*
[first](732d308c8d)
adds cosmetic fixes to remove backtraces from logs yet again and print
paths in quotes, as file descriptors may return empty paths.
It also stubs the cause if OOM in project panel: that one traversed all
worktrees in `for worktree_snapshot in visible_worktrees` and "accepted"
the one with empty paths + never called `entry_iter.advance();` in "no
file name found for the worktree" case, thus looping endlessly and
bloating the memory quite fast.

*
[second](7ebfe5da2f)
adds something that resembles a fix: `fn current_path` on macOS used the
file handler to re-fetch the worktree root file path on worktree root
canonicalization failure.
What's odd, is that `libc::fcntl` returns `0` in the case when external
volume is not mounted, thus resulting in the `""` path string that is
propagated all the way up.

*
[third](1a7560cef3)
moves the fix down to the platform-related FS implementations

The "fix" now checks the only usage of this method inside `async fn
process_events` for an empty path and bails if that is the case.
I am not sure what is a better fix, but this stops any memory leaks and
given how bad the situation now, seems ok to merge for now with the
`TODO` comment for more clever people to fix properly later.

----------------

Now, when I disconnect the SMB share and reconnect it again, Zed stops
displaying any files in the project tree but the ones opened as editors.

As before, at first, when the share is unmounted, Zed fails to save any
changes because of the timeouts.

Later, when the share is re-connected, macOS Finder hangs still but Zed
starts to react on saves yet still only shows the files that are open as
editors.
The files can be edited and saved from now on.

Later, when Finder finally stops hanging and indicates that the share is
mounted fully, the rest of the file structure reappear in the project
panel, and all file saves are propagated, hence can be observed in the
share in Finder.

It feels that one good improvement to add on top is some "disconnected"
indicator that clearly shows that the file is not properly handles in
the OS.
This requires much more changes and thinking as nothing like that exists
in Zed yet, hence not done.

Release Notes:

- Fixed Zed OOM-ing when macOS file descriptors become invalid
2025-12-26 18:47:53 +00:00
Agus Zubiaga
f1b723973b mac: Delay initial find pasteboard search until ⌘G or ⌘F (#45605)
Follow up to https://github.com/zed-industries/zed/pull/45311. Instead
of searching for the string in the find pasteboard as soon as the pane
is focused, we will now wait until the search bar is either deployed or
`Select{Next|Prev}Match` is triggered.

Release Notes:

- N/A
2025-12-26 15:22:57 -03:00
Nathan Sobo
a7ce677ac3 Optimize terminal rendering when clipped by parent containers (#45537)
This brings the terminal element's viewport culling in line with the
editor optimization in PR #44995 and the fix in PR #45077.

## Problem

When a terminal is inside a scrollable container (e.g., the Agent Panel
thread view), it would render ALL cells during prepaint, even when the
terminal was entirely outside the viewport. This caused unnecessary CPU
usage when multiple terminal tool outputs existed in the Agent Panel.

## Solution

Calculate the intersection of the terminal's bounds with the current
content_mask (the visible viewport after all parent clipping). If the
intersection has zero area, skip all cell processing entirely.

### Three code paths

1. **Offscreen** (`intersection.size <= 0`): Early exit, process 0 cells
2. **Fully visible** (`intersection == bounds`): Fast path, stream cells
directly (no allocation)
3. **Partially clipped**: Group cells by line, skip/take visible rows
only

### Key insight: filter by screen position, not buffer coordinates

The previous approach tried to filter cells by `cell.point.line`
(terminal buffer coordinates), which breaks in Scrollable mode where
cells can have negative line numbers for scrollback history.

The new approach filters by **screen position** using
`chunk_by(line).skip(N).take(M)`, which works regardless of the actual
line numbers because we're filtering on enumerated line group index.

## Testing

Added comprehensive unit tests for:
- Screen-position filtering with positive lines (Inline mode)
- Screen-position filtering with negative lines (Scrollable mode with
scrollback)
- Edge cases (skip all, positioning math)
- Unified filtering works for both modes

Manually verified:
- Terminal fully visible (no clipping) ✓
- Terminal clipped from top/bottom ✓
- Terminal completely outside viewport ✓
- Scrollable terminals with scrollback history ✓
- Selection/interaction still works ✓

Release Notes:

- Improved Agent Panel performance when terminals are scrolled
offscreen.

/cc @as-cii
2025-12-26 11:20:40 -07:00
Finn Evers
ed67f246cb Fix formatting in json_schema_store.rs (#45698)
There are some too long lines which make `rustfmt` unable to format the
file, which in turn makes editing and working with this file rather
hard. This PR fixes this.

Release Notes:

- N/A
2025-12-26 18:10:33 +00:00
Marshall Bowers
93f29326c4 docs: Update link to Tree-sitter Query extension (#45697)
Release Notes:

- N/A
2025-12-26 17:53:40 +00:00
Haojian Wu
85f4681299 docs: Link to Tree-sitter query extension (#45682)
Release Notes:

- N/A
2025-12-26 17:43:18 +00:00
Finn Evers
741c5d5010 Revive "good first issue" notifier (#45679)
We adjusted the labels some time ago, but never took care of the `good
first issue` notifier that posts the good first issues to discord.

Adjusting the label accordingly so that it notifies people again.

Release Notes:

- N/A
2025-12-26 10:16:32 +00:00
Marco Mihai Condrache
f03987fb68 search: Remove intermediate allocation (#45633)
Release Notes:

- N/A

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-12-25 20:29:17 +01:00
Teoh Han Hui
ca47822667 Associate devcontainer.json with JSONC language (#45593)
Release Notes:

- N/A
2025-12-23 21:23:28 +00:00
Danilo Leal
a34fe06bb1 agent_ui: Allow "token reached" callout to be dismissed (#45595)
It was previously impossible to dismiss the "token usage
reaching/reached the limit" callout.

<img width="500" height="392" alt="Screenshot 2025-12-23 at 5  49@2x"
src="https://github.com/user-attachments/assets/7fd8b126-dd3f-430b-9fea-ca05c73e5643"
/>

Release Notes:

- N/A
2025-12-23 21:14:58 +00:00
Kirill Bulatov
0ce484e66c Do not trust Docker hosts by default (#45587)
It's still possible to leak secrets by spawning odd MCP/LSP servers from
`.zed/settings.json`

Release Notes:

- N/A
2025-12-23 19:27:09 +00:00
Kirill Bulatov
251033f88f Fix the argument order when starting devcontainers (#45584)
Release Notes:

- (Preview only) Fix devcontainers not starting when certain env
variables were set

Co-authored-by: KyleBarton <kjb@initialcapacity.io>
2025-12-23 19:10:51 +00:00
Xiaobo Liu
9f90c1a1b7 git_ui: Show copy-SHA button on commit header hover (#45478)
Release Notes:

- git: Added the ability to copy a commit's SHA in the commit view.

---------

Signed-off-by: Xiaobo Liu <cppcoffee@gmail.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-23 17:11:56 +00:00
Danilo Leal
d43cc46288 agent_ui: Add more items in the right-click context menu (#45575)
Follow up to https://github.com/zed-industries/zed/pull/45440 adding an
item for "Open Thread as Markdown" and another for scroll to top and
scroll to bottom.

<img width="500" height="646" alt="Screenshot 2025-12-23 at 1  12@2x"
src="https://github.com/user-attachments/assets/c82e26bb-c255-4d73-b733-ef6ea269fabe"
/>

Release Notes:

- N/A
2025-12-23 13:22:42 -03:00
Daniel Byiringiro
fdb8e71b43 docs: Remove reference to outdated curated issues board (#45568)
The documentation referenced a “Curated board of issues” GitHub Project
that no longer exists.
The linked project returns a 404, and only three public projects are
currently available under
zed-industries.

This PR removes the outdated reference. Documentation-only change.

Release Notes:

- N/A
2025-12-23 15:15:58 +00:00
zchira
6bc433ed43 agent_ui: Add right-click context menu to the thread view (#45440)
Closes #23158

Release Notes:

- Added a right-click context menu for the thread view in the agent
panel.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-23 12:09:46 -03:00
Danilo Leal
1281f4672c markdown: Add support for right-click menu copy item (#45572)
In https://github.com/zed-industries/zed/pull/45440, we're implementing
the ability to right-click in the agent panel and copy the rendered
markdown. However, that presented itself as not as straightforward as
just making the menu item fire the `CopyAsMarkdown` action because any
selection in markdown is cleared after a new mouse click, and for the
right-click copy menu item to work, we need to persist that selection
even after the menu itself is opened and the "Copy" menu item is
clicked.

This all demanded a bit of work in the markdown file itself, and given
we may want to use this functionality for other non-agent thread view
markdown use cases in the future, I felt like it'd be better breaking it
down into a separate PR that we can more easily track in the future.

The context menu still needs to be built in the place where the markdown
is created and rendered, though. This PR only adds the infrastructure
needed so that this menu can simply fire the `CopyAsMarkdown` and make
the copying work.

Release Notes:

- N/A
2025-12-23 12:09:10 -03:00
Rocky Shi
ed705c0cbc Conditionally display debugger panel icon based on a setting (#45544)
Closes [#ISSUE](https://github.com/zed-industries/zed/issues/45506)

Release Notes:

- Conditionally display the debugger panel icon based on a setting to
avoid too many error logs
2025-12-23 13:28:04 +01:00
Joseph T. Lyons
8980333e23 Add support for automatic Markdown task list continuation when using uppercase X (#45561)
Release Notes:

- Added support for automatic Markdown task list continuation when using
uppercase X
2025-12-23 08:07:48 +00:00
Cole Miller
acee48bfda git: Fix "Commit Tracked" being shown when files are partially staged (#45551)
Release Notes:

- N/A
2025-12-22 21:32:55 -05:00
Finn Evers
71298e6949 extension_ci: Use larger runners for extension bundling (#45540)
`2x4` is not nearly enough for some of the grammars in use, hence change
this to a larger runner.

Also, reduce the size for the Rust runners a bit, as they don't need to
be quite as large for the amount of Rust code we have in extensions.

Release Notes:

- N/A
2025-12-22 22:08:42 +00:00
Max Brunsfeld
07ada58466 Improve edit prediction example capture (#45536)
This PR improves the `edit prediction: Capture Example` in several ways:
* fixed bugs in how the uncommitted diff was calculated
* added a `edit_predictions.examples_dir` setting that can be set in
order to have the action automatically save examples into the given
folder
* moved the action into the `edit_predictions` crate, in preparation for
collecting this data passively from end users, when they have opted in
to data sharing, similar to what we did for Zeta 1

Release Notes:

- N/A
2025-12-22 20:40:02 +00:00
Kirill Bulatov
dd521a96fb Bump proto extension to 0.3.1 (#45531)
Includes https://github.com/zed-industries/zed/pull/45413

Release Notes:

- N/A
2025-12-22 18:40:27 +00:00
Danilo Leal
f9d9721b93 agent_ui: Expand model favoriting feature to external agents (#45528)
This PR adds the ability to favorite models for external agents—writing
to the settings in the `agent_servers` key—as well as a handful of other
improvements:

- Make the cycling keybinding `alt-enter` work for the inline assistant
as well as previous user messages
- Better organized the keybinding files removing some outdated
agent-related keybinding definitions
- Renamed the inline assistant key context to "InlineAssistant" as
"PromptEditor" is old and confusing
- Made the keybindings to rate an inline assistant response visible in
the thumbs up/down button's tooltip
- Created a unified component for the model selector tooltip given we
had 3 different places creating the same element
- Make the "Cycle Favorited Models" row in the tooltip visible only if
there is more than one favorite models

Release Notes:

- agent: External agents also now support the favoriting model feature,
which comes with a handy keybinding to cycle through the favorite list.
2025-12-22 14:06:54 -03:00
Alejandro Fernández Gómez
cff3ac6f93 docs: Fix download_file documentation (#45517)
Fix a small error in the docs for the extension capabilities

Release Notes:

- N/A
2025-12-22 10:17:26 +00:00
Finn Evers
746b76488c util: Keep default permissions when extracting Zip with unset permissions (#45515)
This ensures that we do not extract files with no permissions (`0o000`),
because these would become unusable on the host

Release Notes:

- N/A
2025-12-22 09:28:11 +00:00
Marshall Bowers
397fcf6083 docs: Fix Edit Prediction docs for Codestral (#45509)
This PR fixes the Edit Prediction docs for Codestral after they got
mangled in https://github.com/zed-industries/zed/pull/45503.

Release Notes:

- N/A
2025-12-22 04:10:26 +00:00
morgankrey
9adb3e1daa docs: Testing automatic documentation updates locally (2025-12-21) (#45503)
## Documentation Update Summary

### Changes Made

| File | Change | Related Code |
| --- | --- | --- |
| `docs/src/ai/edit-prediction.md` | Updated Codestral setup
instructions to use Settings Editor path instead of outdated
`agent::OpenSettings` action reference | Settings Editor provider
configuration flow |

### Rationale

The primary documentation update addresses outdated instructions in the
Codestral setup section. The original text referenced an
`agent::OpenSettings` action that directed users to an "Agent Panel
settings view" which no longer reflects the current UI flow. The updated
instructions now guide users through the Settings Editor with
platform-specific keyboard shortcuts and provide an alternative status
bar path.

### Review Notes

- **Codestral instructions**: Reviewers should verify the Settings
Editor navigation path (`Cmd+,` → search "Edit Predictions" →
**Configure Providers**) matches the current Zed UI
- **Status bar alternative**: The alternative path via "edit prediction
icon in the status bar" should be confirmed as accurate

---

## Update from 2025-12-21 20:25

---
**Source**: [#44914](https://github.com/zed-industries/zed/pull/44914) -
settings_ui: Add Edit keybindings button
**Author**: @probably-neb

Now I have all the context needed to create a comprehensive
documentation update summary.

## Documentation Update Summary

### Changes Made
| File | Change | Related Code |
| --- | --- | --- |
| docs/src/ai/agent-panel.md | Added documentation for `agent::PasteRaw`
action, explaining automatic @mention formatting for pasted code and how
to bypass it | PR #45254 |

### Rationale
PR #45254 ("agent_ui: Improve UX when pasting code into message editor")
introduced the `agent::PasteRaw` action, which allows users to paste
clipboard content without automatic formatting. When users copy
multi-line code from an editor buffer and paste it into the Agent panel,
Zed now automatically formats it as an @mention with file context. The
`PasteRaw` action provides a way to bypass this behavior when raw text
is preferred.

This documentation update ensures users can discover both:
1. The new automatic @mention formatting behavior
2. The keybinding to bypass it when needed

### Review Notes
- The new paragraph was placed in the "Adding Context" section,
immediately after the existing note about image pasting support—this
maintains logical flow since both relate to pasting behavior
- Uses the standard `{#kb agent::PasteRaw}` syntax for keybinding
references, consistent with other keybinding documentation in the file
- The documentation passed Prettier formatting validation without
modifications

---

### Condensed Version (for commit message)
```
docs(agent-panel): Document PasteRaw action for bypassing auto @mention formatting

Added explanation that multi-line code pasted from editor buffers is
automatically formatted as @mentions, with keybinding to paste raw text.

Related: PR #45254
```

Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
2025-12-21 20:31:24 -06:00
Daeksell
1469d94683 Fix dock panel button tooltip not dismissed when state changes via keyboard shortcut (#44746)
Closes #44720

Release Notes:

- Fixed dock panel button tooltips not being dismissed when toggling
panels via keyboard shortcut




**Problem:** When hovering over a dock panel button and using a keyboard
shortcut to toggle the panel, the tooltip remains visible with stale
content. This is inconsistent with mouse click behavior, where the
tooltip is dismissed on mouse down.

**Solution:** Include the panel's active state in the button's element
ID. When the state changes, the element ID changes (e.g., `"DebugPanel"`
→ `"DebugPanel-active"`), which causes GPUI to discard the old element
state including the cached tooltip.

**Testing:** Manually verified:
1. Hover over a dock panel button, wait for tooltip
2. Press keyboard shortcut to toggle the panel
3. Tooltip is now dismissed (consistent with mouse click behavior)


https://github.com/user-attachments/assets/ed92fb6c-6c22-44e2-87e3-5461d35f7106

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-12-22 00:50:54 +01:00
Yves Ineichen
3b626c8ac1 Allow empty splits on panes (#40245)
Draft as a base for continuing the discussion in #8008 : adds a
`SplitOperation` enum to support bindings like `["pane::SplitLeft",
{"operation": "Clear"}]`

To be discussed @MrSubidubi and others:

- Naming: Generally not happy with names yet and specifically `Empty` is
unclear, e.g., what does this mean for terminal panes? Added placeholder
code to split without cloning, but unsure what users would expect in
this case.
- ~~I removed `SplitAndMoveXyz` actions but I guess we should keep them
for backwards compatibility?~~
- May have missed details in the move implementation. Will check the
code again for opportunities to refactor more code after we agree on the
approach.
- ~~Tests should go to `crates/collab/src/tests/integration_tests.rs`?~~

Closes #8008

Release Notes:

- Add `pane::Split` mode (`{ClonePane,EmptyPane,MovePane}`) to allow
creating an empty buffer.

---------

Co-authored-by: Finn Evers <finn.evers@outlook.de>
Co-authored-by: MrSubidubi <finn@zed.dev>
2025-12-21 23:50:02 +00:00
Kirill Bulatov
3dc0614dba Small worktree trust fixes (#45500)
* Abs path trust should transitively trust all single file worktrees on
the same host
* Init worktree trust on the client side even when devcontainers are
run: remote host unconditionally checks trust, hence the client has to
keep track of it and respond with approves/declines.
Do trust all devcontainers' remote worktrees, as containers are isolated
and "safe".

Release Notes:

- N/A
2025-12-21 23:07:49 +00:00
Mayank Verma
045e154915 gpui: Fix hover state getting stuck when rapidly hovering over elements (#45437)
Closes #45436

Release Notes:

- N/A

---------

Co-authored-by: MrSubidubi <finn@zed.dev>
2025-12-21 22:07:30 +00:00
Finn Evers
dc72e1c4ba collab: Fix capitalization of copilot name alias (#45497)
This fixes copilot currently not passing the CLA check. 

Release Notes:

- N/A
2025-12-21 21:36:54 +00:00
Lukas Wirth
0884305e43 gpui(windows): Don't log incorrect errors on SetActiveWindow calls (#45493)
The function returns the previous focus handle, which may be null if
there is no previous focus. Unfortunately that also overlaps with the
error return value, so winapi will hand us a error 0 back in those cases
which we log ...


Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-21 16:14:23 +00:00
Nereuxofficial
83449293b6 Add autocomplete for initialization_options (#43104)
Closes #18287

Release Notes:

- Added autocomplete for lsp initialization_options

## Description
This MR adds the following code-changes:
- `initialization_options_schema` to the `LspAdapter` to get JSON
Schema's from the language server
- Adds a post-processing step to inject schema request paths into the
settings schema in `SettingsStore::json_schema`
- Adds an implementation for fetching the schema for rust-analyzer which
fetches it from the binary it is provided with
- Similarly for ruff
<img width="857" height="836" alt="image"
src="https://github.com/user-attachments/assets/3cc10883-364f-4f04-b3b9-3c3881f64252"
/>


## Open Questions(Would be nice to get some advice here)
- Binary Fetching:
- I'm pretty sure the binary fetching is suboptimal. The main problem
here was getting access to the delegate but i figured that out
eventually in a way that i _hope_ should be fine.
- The toolchain and binary options can differ from what the user has
configured potentially leading to mismatches in the autocomplete values
returned(these are probably rarely changed though). I could not really
find a way to fetch these in this context so the provided ones are for
now just `default` values.
- For the trait API it is just provided a binary, since i wanted to use
the potentially cached binary from the CachedLspAdapter. Is that fine
our should the arguments be passed to the LspAdapter such that it can
potentially download the LSP?
- As for those LSPs with JSON schema files in their repositories i can
add the files to zed manually e.g. in
languages/language/initialization_options_schema.json, which could cause
mismatches with the actual binary. Is there a preferred approach for Zed
here also with regards to updating them?
2025-12-21 10:29:38 -05:00
Marco Mihai Condrache
213cb30445 gpui: Enable direct-to-display optimization for metal (#45434)
Continuing of #44334 

I removed disabling of vsync which was causing jitter on some external
displays

cc: @maxbrunsfeld @Anthony-Eid 

Release Notes:

- Mark metal layers opaque for non-transparent windows to allow
direct-to-display when supported

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-12-21 11:17:02 +02:00
Finn Evers
4b56fec971 acp_thread: Fix broken main build (#45461)
Release Notes:

- N/A
2025-12-20 20:01:03 +00:00
Nathan Sobo
32621dc5de Fix race condition in update_last_checkpoint (#44801)
Release Notes:

- Fixed spurious "no checkpoint" error in agent panel

---

## Summary

`update_last_checkpoint` would call `last_user_message()` twice - once
at the start to capture the checkpoint, and again in an async closure
after the checkpoint comparison completed. If a new user message without
a checkpoint was added between these two calls, the second call would
find the new message and fail with "no checkpoint".

## Fix

Capture the user message ID at the start and use `user_message_mut(&id)`
in the async closure to find the specific message.

cc @mikayla-maki
2025-12-20 10:42:58 -07:00
Danilo Leal
215ac50bc8 agent_ui: Fix markdown block for tool call input and output content (#45454)
This PR fixes two issues with regards to markdown codeblocks rendered in
tool call input and output content display:
- the JSON code snippets weren't properly indented
- codeblocks weren't being rendered in unique containers; e.g., if you
hovered one scrollbar, all of them would also be hovered, even though
horizontal scrolling itself worked properly

Here's the end result:


https://github.com/user-attachments/assets/3d6daf64-0f88-4a16-a5a0-94998c1ba7e2

Release Notes:

- agent: Fix scrollbar and JSON indentation for tool call input/output
content's markdown codeblocks.
2025-12-20 16:29:13 +00:00
Danilo Leal
a5540a08fb ui: Make the NumberField in edit mode work (#45447)
- Make the buttons capable of changing the editor's content
(incrementing or decrementing the value)
- Make arrow key up and down increment and decrement the editor value
- Tried to apply a bit of DRY here by creating some functions that can
be reused across the buttons and editor given they all essentially do
the same thing (change the value)
- Fixed an issue where the editor would not allow focus to move
elsewhere, making it impossible to open a dropdown, for example, if your
focus was on the number field's editor

Release Notes:

- N/A
2025-12-20 11:15:46 -03:00
ᴀᴍᴛᴏᴀᴇʀ
3e8c25f5a9 Remove extra shortcut separator in default mode & model selection tooltips (#45439)
Closes #44118

Release Notes:

- N/A
2025-12-20 14:08:56 +00:00
Zachiah Sawyer
7f0842e3a6 proto: Add extend keyword (#45413)
Closes #45385

Release Notes:
- Add extend keyword to proto
2025-12-20 08:00:04 +02:00
Hidehiro Anto
6dad419cd5 Update version example in remote-development.md (#45427)
Updated the version example for maintaining the remote server binary.

Release Notes:

- N/A
2025-12-20 05:51:22 +00:00
Danilo Leal
0facdfa5ca editor: Make TextAlign::Center and TextAlign::Right work (#45417)
Closes https://github.com/zed-industries/zed/issues/43208

This PR essentially unblocks the editable number field. The function
that shapes editor lines was hard-coding text alignment to the left,
meaning that whatever different alignment we'd pass through
`EditorStyles`would be ignored. To solve this, I just added a text align
and align width fields to the line paint function and updated all call
sites keeping the default configuration. Had to also add an
`alignment_offset()` helper to make sure the cursor positioning, the
selection background element, and the click-to-focus functionality were
kept in-sync with the non-left aligned editor.

Then... the big star of the show here is being able to add the `mode`
method to the number field, which uses `TextAlign::Center`, thus making
it work as we designed it to work.


https://github.com/user-attachments/assets/3539c976-d7bf-4d94-8188-a14328f94fbf

Next up, is turning the number filed to edit mode where applicable.

Release Notes:

- Fixed a bug where different text alignment configurations (i.e.,
center and right-aligned) wouldn't take effect in editors.
2025-12-19 17:37:15 -08:00
Marshall Bowers
58461377ca ci: Disable automated docs on pushes to main (#45416)
This PR disables the automated docs on pushes to `main`, as it is
currently making CI red.

Release Notes:

- N/A
2025-12-20 01:01:19 +00:00
Lieunoir
42d5f7e73e Set override_redirect for PopUps (#42224)
Currently on x11, gpui PopUp windows only rely on the "notification"
type in order to indicate that they should spawn as floating window.
Several window managers (leftwm in my case, but it also seems to be the
case for dwm and ratpoison) do not this property into account thus not
spawning them as float. On the other hand, using Floating instead of
PopUp do make those windows spawn as floating, as these window manager
do take into account the (older) "dialog" type.

The [freedekstop
documentation](https://specifications.freedesktop.org/wm/1.5/ar01s05.html#id-1.6.7)
does seem to suggest that these windows should also have the override
redirect property :
> This property is typically used on override-redirect windows. 

Note that this also disables pretty much all interactions with the
window manager (such as moving the window, resizing etc...)

Release Notes:

- Fix popup windows not spawning floating sometime on x11
2025-12-19 16:38:45 -08:00
Mikayla Maki
5395197619 Separate out component_preview crate and add easy-to-use example binaries (#45382)
Release Notes:

- N/A
2025-12-20 00:34:14 +00:00
Mayank Verma
1d76539d28 gpui: Fix hover styles not being applied during layout (#43324)
Closes #43214

Release Notes:

- Fixed GPUI hover styles not being applied during layout

Here's the before/after:


https://github.com/user-attachments/assets/5b1828bb-234a-493b-a33d-368ca01a773b
2025-12-19 16:33:32 -08:00
jkugs
e5eb26e8d6 gpui: Reset mouse scroll state on FocusOut to prevent large jumps (#43841)
This fixes an X11 scrolling issue where Zed may jump by a large amount
due to the scroll valuator state not being reset when the window loses
focus. If you Alt-Tab away from Zed, scroll in another application, then
return, the first scroll event in Zed applies the entire accumulated
delta instead of a single step.

The missing FocusOut reset was originally identified in issue #34901.

Resetting scroll positions on FocusOut matches the behavior already
implemented in the XinputLeave handler and prevents this jump.

Closes #34901
Closes #40538

Release Notes:

- Fixed an X11 issue where Alt-Tabbing to another application,
scrolling, and returning to Zed could cause the next scroll event to
jump by a large amount.
2025-12-19 16:29:44 -08:00
Serophots
a86b0ab2e0 gpui: Improve the tab stop example by demonstrating tab_group (#44647)
I've just enriched the existing tab_stop.rs example for GPUI with a
demonstration of tab_group. I don't think tab groups existed when the
original example was written.

(I didn't understand the behaviour for tab_group from the doccomments
and the example was missing, so I think this is a productive PR)

Release Notes:

- N/A
2025-12-20 00:29:26 +00:00
Jason Lee
5fb220a19a gpui: Add a Popover example for test deferred (#44473)
Release Notes:

- N/A

<img width="1036" height="659" alt="image"
src="https://github.com/user-attachments/assets/8ca06306-719f-4495-92b3-2a609aa09249"
/>
2025-12-19 16:27:41 -08:00
Anthony Eid
12dbbdd1d3 git: Fix bug where opening a git blob from historic commit view could fail (#44226)
The failure would happen if the current version of the file was open as
an editor. This happened because the git blob and current version of the
buffer would have the same `ProjectPath`.

The fix was adding a new `DiskState::Historic` variant to represent
buffers that are past versions of a file (usually a snapshot from
version control). Historic buffers don't return a `ProjectPath` because
the file isn't real, thus there isn't and shouldn't be a `ProjectPath`
to it. (At least with the current way we represent a project path)

I also change the display name to use the local OS's path style instead
of being hardcoded to Posix, and cleaned up some code too.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: xipengjin <jinxp18@gmail.com>
2025-12-19 18:55:17 -05:00
Max Brunsfeld
6dfabddbb4 Revert "gpui: Enable direct-to-display optimization for metal" (#45405)
Reverts zed-industries/zed#44334

From my testing, this PR introduced screen tearing, or some kind of
strange visual artifact, when scrolling at medium speed on a large
display.

Release notes:

- N/A
2025-12-19 15:21:10 -08:00
Haojian Wu
895213a94d Support union declarations in C/C++ textobjects.scm (#45308)
Release Notes:

- C/C++: Add `union` declarations to the list of text objects
2025-12-19 17:37:13 -05:00
Richard Feldman
1c576ccf82 Fix OpenRouter giving errors for some Anthropic models (#45399)
Fixes #44032

Release Notes:

- Fix OpenRouter giving errors for some Anthropic models
2025-12-19 17:04:43 -05:00
Anthony Eid
3f4da03d38 settings ui: Change window kind from floating to normal (#45401)
#40291 made floating windows always stay on top, which made the settings
ui window always on top of Zed. To maintain the old behavior, this PR
changes the setting window to be a normal window.

Release Notes:

- N/A
2025-12-19 16:48:53 -05:00
Conrad Irwin
ff71f4d46d Run cargo fix as well as cargo clippy --fix (#45394)
Release Notes:

- N/A
2025-12-19 14:27:44 -07:00
morgankrey
71f4dc2481 docs: Stash local changes before branch checkout in droid auto docs CLI (#45395)
Stashes local changes before branch checkout in droid auto docs CLI

Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 14:10:16 -06:00
Nathan Sobo
b091cc4d9a Enforce 5MB per-image limit when converting images for language models (#45313)
## Problem

When users paste or drag large images into the agent panel, the encoded
payload can exceed upstream provider limits (e.g., Anthropic's 5MB
per-image limit), causing API errors.

## Solution

Enforce a default 5MB limit on encoded PNG bytes in
`LanguageModelImage::from_image`:

1. Apply existing Anthropic dimension limits first (1568px max in either
dimension)
2. Iteratively downscale by ~15% per pass until the encoded PNG is under
5MB
3. Return `None` if the image can't be shrunk within 8 passes
(fail-safe)

The limit is enforced at the `LanguageModelImage` conversion layer,
which is the choke point for all image ingestion paths (agent panel
paste/drag, file mentions, text threads, etc.).

## Future Work

The 5MB limit is a conservative default. Provider-specific limits can be
introduced later by adding a `from_image_with_constraints` API.

## Testing

Added a regression test that:
1. Generates a noisy 4096x4096 PNG (guaranteed >5MB)
2. Converts it via `LanguageModelImage::from_image`
3. Asserts the result is ≤5MB and was actually downscaled

---

**Note:** This PR builds on #45312 (prompt store fail-open fix). Please
merge that first.

cc @rtfeldman

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
2025-12-19 15:04:41 -05:00
Nathan Sobo
8e5d33ebc6 Make prompt store fail-open when DB contains undecodable records (#45312)
Release Notes

- N/A

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
2025-12-19 14:59:01 -05:00
morgankrey
99224ccc75 docs: Droid needs a real model (#45393)
Droid needs a specific model with a date
Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 13:43:10 -06:00
Michael Benfield
56646e6bc3 Inline assistant: Don't scroll up too high (#45171)
In the case of large vertical_scroll_margin, we could scroll up such
that the assistant was out of view. Now, keep it no lower than the
center of the editor.

Closes #18058

Release Notes:

- N/A
2025-12-19 11:37:57 -08:00
morgankrey
bb2f037407 docs: Droid doesn't know its own commands (#45391)
Correctly uses droid commands in auto docs actions

Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 13:20:05 -06:00
Cole Miller
07db88a327 git: Optimistically stage hunks when staging a file, take 2 (#45278)
Relanding #43434 with an improved approach.

Release Notes:

- N/A

---------

Co-authored-by: Ramon <55579979+van-sprundel@users.noreply.github.com>
2025-12-19 19:08:49 +00:00
morgankrey
e61f9081d4 docs: More droid docs debugging (#45388)
Path issues

Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 12:24:23 -06:00
Ichimura Tomoo
1bc3fa8154 Correct UTF-16 saving and add heuristic encoding detection (#45243)
This commit fixes an issue where saving UTF-16 files resulted in UTF-8
bytes due to `encoding_rs` default behavior. It also introduces a
heuristic to detect BOM-less UTF-16 and binary files.

Changes:
- Manually implement UTF-16LE/BE encoding during file save to avoid
implicit UTF-8 conversion.
- Add `analyze_byte_content` to guess UTF-16LE/BE or Binary based on
null byte distribution.
- Prevent loading binary files as text by returning an error when binary
content is detected.

Special thanks to @CrazyboyQCD for pointing out the `encoding_rs`
behavior and providing the fix, and to @ConradIrwin for the suggestion
on the detection heuristic.

Closes #14654

Release Notes:

- (nightly only) Fixed an issue where saving files with UTF-16 encoding
incorrectly wrote them as UTF-8. Also improved detection for binary
files and BOM-less UTF-16.
2025-12-19 18:18:20 +00:00
morgankrey
22916311cd ci: Fix Factory CLI installation URL (#45386)
Change from cli.factory.ai/install.sh to app.factory.ai/cli per official
Factory documentation.

Release Notes:

- N/A

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 12:12:14 -06:00
Miguel Raz Guzmán Macedo
1edd050baf Add script/triage_watcher.jl (#45384)
Release Notes:

- N/A
2025-12-19 18:09:40 +00:00
morgankrey
b53f661515 docs: Fix auto docs GitHub Action (#45383)
Small fixes to Droid workflow

Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 11:53:39 -06:00
Andrew Farkas
4ef5d2c814 Fix relative line numbers in sticky headers (#45164)
Closes #42586 

This includes a rewrite of `calculate_relative_line_numbers()`. Now it's
linear-time with respect to the number of rows displayed, instead of
linear time with respect to the number of rows displayed _plus_ the
distance to the base row.

Release Notes:

- Improved performance when using relative line numbers in large files
- Fixed relative line numbers not appearing in sticky headers
2025-12-19 17:32:38 +00:00
morgankrey
bfe3c66c3e docs: Automatic Documentation Github Action using Droid (#45374)
Adds a multi-step agentic loop to github actions for opening a
once-daily documentation PR that can be merged only be a Zedi

Release Notes:

- N/A

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-19 11:19:12 -06:00
Julia Ryan
361b8e0ba9 Fix sticky header scroll offset (#45377)
Closes #43319

Release Notes:

- Sticky headers no longer obscure the cursor when it moves.

---------

Co-authored-by: HactarCE <6060305+HactarCE@users.noreply.github.com>
2025-12-19 09:04:15 -08:00
Agus Zubiaga
d7e41f74fb search: Respect macOS' find pasteboard (#45311)
Closes #17467

Release Notes:

- On macOS, buffer search now syncs with the system find pasteboard,
allowing <kbd>⌘E</kbd> and <kbd>⌘G</kbd> to work seamlessly across Zed
and other apps.
2025-12-19 13:31:27 -03:00
Ben Kunkle
e05dcecac4 Make pane::CloseAllItems best effort (#45368)
Closes #ISSUE

Release Notes:

- Fixed an issue where the `pane: close all items` action would give up
if you hit "Cancel" on the prompt for what to do with a dirty buffer
2025-12-19 16:21:56 +00:00
Danilo Leal
32600f255a gpui: Fix truncation flickering (#45373)
It's been a little that we've noticed some flickering and other weird
resizing behavior with text truncation in Zed:


https://github.com/user-attachments/assets/4d5691a3-cd3d-45e0-8b96-74a4e0e273d2


https://github.com/user-attachments/assets/d1d0e587-7676-4da0-8818-f4e50f0e294e

Initially, we suspected this could be due to how we calculate the length
of a line to insert truncation, which is based first on the length of
each individual character, and then second goes through a pass
calculating the line length as a whole. This could cause mismatch and
culminate in our bug.

However, even though that felt like a reasonable suspicion, I realized
something rather simple at some point: the `truncate` and
`truncate_start` methods in the `Label` didn't use `whitespace_nowrap`.
If you take Tailwind as an example, their `truncate` utility class takes
`overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`. This
pointed out to a potential bug with `whitespace_nowrap` where that was
blocking truncation entirely, even though that's technically part of
what's necessary to truncate as you don't want text that will be
truncated to wrap.

Ultimately, what was happening was that the text element was caching its
layout based on its `wrap_width` but not considering its
`truncate_width`. The truncate width is essentially the new definitive
width of the text based on the available space, which was never being
computed. So the fix here was to add `truncate_width.is_none()` to the
cache validation check, so that it only uses the cached text element
size _if the truncation width is untouched_. But if that changes, we
need to account for the new width. Then, in the Label component, we
added `min_w_0` to allow the label div to shrink below its original
size, and finally, we added `whitespace_nowrap()` as the cache check
fundamentally fixed that method's problem.

In a future PR, we can basically remove the `single_line()` label method
because: 1) whenever you want a single label, you most likely want it to
truncate, and 2) most instances of `truncate` are already followed by
`single_line` in Zed today, so we can cut that part.

Result is no flickering with truncated labels!


https://github.com/user-attachments/assets/ae17cbde-0de7-42ca-98a4-22fcb452016b

Release Notes:

- Fixed a bug in GPUI where truncated text would flicker as you resized
the container in which the text was in.

Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
2025-12-19 13:14:31 -03:00
Raduan A.
a7e07010e5 editor: Add automatic markdown list continuation on newline and indent on tab (#42800)
Closes #5089

Release notes:
- Markdown lists now continue automatically when you press Enter
(unordered, ordered, and task lists). This can be configured with
`extend_list_on_newline` (default: true).
- You can now indent list markers with Tab to quickly create nested
lists. This can be configured with `indent_list_on_tab` (default: true).

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
2025-12-19 21:44:02 +05:30
feeiyu
ea34cc5324 Fix terminal doesn't switch to project directory when opening remote project on Windows (#45328)
Closes #45253

Release Notes:

- Fixed terminal doesn't switch to project directory when opening remote
project on Windows
2025-12-19 17:06:16 +01:00
Danilo Leal
a7d43063d4 workspace: Make title bar pickers render nearby the trigger when mouse-triggered (#45361)
From Zed's title bar, you can click on buttons to open three modal
pickers: remote projects, projects, and branches. All of these pickers
use the modal layer, which by default, renders them centered on the UI.
However, a UX issue we've been bothered by is that when you _click_ to
open them, they show up just way too far from where your mouse likely is
(nearby the trigger you just clicked). So, this PR introduces a
`ModalPlacement` enum to the modal layer, so that we can pick between
the "centered" and "anchored" options to render the picker. This way, we
can make the pickers use anchored positioning when triggered through a
mouse click and use the default centered positioning when triggered
through the keybinding.

One thing to note is that the anchored positioning here is not as
polished as regular popovers/dropdowns, because it simply uses the x and
y coordinates of the click to place the picker as opposed to using
GPUI's `Corner` enum, thus making them more connected to their triggers.
I chose to do it this way for now because it's a simpler and more
contained change, given it wouldn't require a tighter connection at the
code level between trigger and picker. But maybe we will want to do that
in the near future because we can bake in some other related behaviors
like automatically hiding the button trigger tooltip if the picker is
open and changing its text color to communicate which button triggered
the open picker.


https://github.com/user-attachments/assets/30d9c26a-24de-4702-8b7d-018b397f77e1

Release Notes:

- Improved the UX of title bar modal pickers (remote projects, projects,
and branches) by making them open closer to the trigger when triggering
them with the mouse.
2025-12-19 13:01:48 -03:00
AidanV
8001877df2 vim: Add :r[ead] [name] command (#45332)
This adds the following Vim commands: 
- `:r[ead] [name]`
- `:{range}r[ead] [name]`

The most important parts of this feature are outlined
[here](https://vimhelp.org/insert.txt.html#%3Ar).

The only intentional difference between this and Vim is that Vim only
allows `:read` (no filename) for buffers with a file attached. I am
allowing it for all buffers because I think that could be useful.

Release Notes:

- vim: Added the [`:r[ead] [name]` Vim
command](https://vimhelp.org/insert.txt.html#:read)

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-12-19 15:31:16 +00:00
Antonio Scandurra
b603372f44 Reduce GPU usage by activating VRR optimization only during high-rate input (#45369)
Fixes #29073

This PR reduces unnecessary GPU usage by being more selective about when
we present frames to prevent display underclocking (VRR optimization).

## Problem

Previously, we would keep presenting frames for 1 second after *any*
input event, regardless of whether it triggered a re-render. This caused
unnecessary GPU work when the user was idle or during low-frequency
interactions.

## Solution

1. **Only track input that triggers re-renders**: We now only record
input timestamps when the input actually causes the window to become
dirty, rather than on every input event.

2. **Rate-based activation**: The VRR optimization now only activates
when input arrives at a high rate (≥ 60fps over the last 100ms). This
means casual mouse movements or occasional keystrokes won't trigger
continuous frame presentation.

3. **Sustained optimization**: Once high-rate input is detected (e.g.,
during scrolling or dragging), we sustain frame presentation for 1
second to prevent display underclocking, even if input briefly pauses.

## Implementation

Added `InputRateTracker` which:
- Tracks input timestamps in a 100ms sliding window
- Activates when the window contains ≥ 6 events (60fps × 0.1s)
- Extends a `sustain_until` timestamp by 1 second each time high rate is
detected

Release Notes:

- Reduced GPU usage when idle by only presenting frames during bursts of
high-frequency input.
2025-12-19 16:06:28 +01:00
Yara 🏳️‍⚧️
7427924405 adjusted scheduler prioritization algorithm (#45367)
This fixes a number of issues where zed depends on the order of polling which changed when switching scheduler. We have adjusted the algorithm so it matches the previous order while keeping the prioritization feature.

Release Notes:
- N/A
2025-12-19 15:03:35 +00:00
Cole Miller
ae44c3c881 Fix extra terminal being created when a task replaces a terminal in the center pane (#45317)
Closes https://github.com/zed-industries/zed/issues/21144

Release Notes:

- Fixed spawned tasks creating an extra terminal in the dock in some
cases.
2025-12-19 09:39:58 -05:00
ozzy
4e0471cf66 git panel: Truncate file paths from the left (#43462)
https://github.com/user-attachments/assets/758e1ec9-6c34-4e13-b605-cf00c18ca16f

Release Notes:

- Improved: Git panel now truncates long file paths from the left,
showing "…path/filename" when space is limited, keeping filenames always
visible.

@cole-miller @mattermill

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-19 13:50:35 +00:00
Danilo Leal
62d36b22fd gpui: Add text_ellipsis_start method (#45122)
This PR is an additive change introducing the `truncate_start` method to
labels, which gives us the ability to add an ellipsis at the beginning
of the text as opposed to the regular `truncate`. This will be generally
used for truncating file paths, where the end is typically more relevant
than the beginning, but given it's a general method, there's the
possibility to be used anywhere else, too.

<img width="500" height="690" alt="Screenshot 2025-12-17 at 12  35@2x"
src="https://github.com/user-attachments/assets/f853f5a3-60b3-4380-a11c-bb47868a4470"
/>

Release Notes:

- N/A

---------

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-12-19 10:25:19 -03:00
Piotr Osiewicz
69f6eeaa3a toolchains: Fix persistence by not relying on unstable worktree id (#45357)
Closes #42268
We've migrated user selections when a given workspace has a single
worktree (as then we could determine what the target worktree is).

Release Notes:

- python: Fixed selected virtual environments not being
persisted/deserialized correctly within long-running Zed sessions (where
multiple different projects might've been opened). This is a breaking
change for users of multi-worktree projects - your selected toolchain
for those projects will be reset.

Co-authored-by: Dino <dino@zed.dev>
2025-12-19 14:06:15 +01:00
Jakub Konka
1dc5de4592 workspace: Auto-switch git context when focus changed (#45354)
Closes #44955 

Release Notes:

- Fixed workspace incorrectly automatically switching Git
repository/branch context in multi-repository projects when repo/branch
switched manually from the Git panel.
2025-12-19 13:54:30 +01:00
Lena
b9aef75f2d Turn on the fixed stalebot (#45355)
It will run weekly and it promised not to touch issues of the wrong
types anymore.

Release Notes:

- N/A
2025-12-19 12:41:03 +00:00
Agus Zubiaga
95ae388c0c Fix title bar spacing when building on the macOS Tahoe SDK (#45351)
The size and spacing around the traffic light buttons changes after
macOS SDK 26. Our official builds aren't using this SDK yet, but dev
builds sometimes are and the official will in the future.

<table>
<tr>
<th>Before</th>
<th>After</th>
</tr>
<tr>
<td>
<img width="582" height="146" alt="CleanShot 2025-12-19 at 08 58 53@2x"
src="https://github.com/user-attachments/assets/1a28d74a-98a3-49d0-98d6-ab05b0580665"
/>
</td>
<td>
<img width="610" height="156" alt="CleanShot 2025-12-19 at 08 57 02@2x"
src="https://github.com/user-attachments/assets/7b7693b3-baa1-4d7e-9fc1-bd7a7bfacd36"
/>
</td>
</tr>
<tr>
<td>
<img width="532" height="154" alt="CleanShot 2025-12-19 at 08 59 40@2x"
src="https://github.com/user-attachments/assets/df7f40e7-7576-44f2-9cf3-047a5d00bb4e"
/>
</td>
<td>
<img width="520" height="150" alt="CleanShot 2025-12-19 at 09 01 17@2x"
src="https://github.com/user-attachments/assets/b0fbdeb6-1b1d-4e7a-95d0-3c78f0569df1"
/>
</td>
</tr>
</table>

Release Notes:

- N/A
2025-12-19 12:19:04 +00:00
Lena
1ac170e663 Upgrade stalebot and make testing it easier (#45350)
- adjust wording for the upcoming simplified process
- upgrade to the github action version that has a fix for configuring issue types the bot should look at
- add two inputs for the manual runs of stalebot that help testing it in a safe and controlled manner 

Release Notes:

- N/A
2025-12-19 12:46:20 +01:00
Angelo Verlain
3104482c6c languages: Detect .bst files as YAML (#45015)
These files are used by the BuildStream build project:
https://buildstream.build/index.html


Release Notes:

- Added recognition for .bst files as yaml.
2025-12-19 10:34:40 +00:00
Piotr Osiewicz
7ee56e1a18 chore: Add worktree_benchmarks to cargo workspace (#45344)
Idk why it was missing, but

Release Notes:

- N/A
2025-12-19 11:18:36 +01:00
Korbin de Man
f2495a6f98 Add Restore File action in project_panel for git modified files (#42490)
Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2025-12-19 10:12:01 +00:00
prayansh_chhablani
6d776c3157 project: Sanitize single-line completions from trailing newlines (#44965)
Closes #43991
trim documentation string to prevent completion overlap


previous
[Screencast from 2025-12-16
14-55-58.webm](https://github.com/user-attachments/assets/d7674d82-63b0-4a85-a90f-b5c5091e4a82)
after change
[Screencast from 2025-12-16
14-50-05.webm](https://github.com/user-attachments/assets/109c22b5-3fff-49c8-a2ec-b1af467d6320)
Release Notes:

- Fixed an issue where completions in the completion menu would span
multiple lines.
2025-12-19 10:11:36 +01:00
Mayank Verma
596826f741 editor: Strip trailing newlines from completion documentation (#45342)
Closes #45337

Release Notes:

- Fixed broken completion menu layout caused by trailing newlines in ty
documentation

<table>
  <tr>
    <td>Before</td>
    <td>After</td>
  </tr>
  <tr>
    <td>
<img width="756" height="875" alt="before"
src="https://github.com/user-attachments/assets/1d9da7d8-437a-4f03-8158-32ff1af9a428"
/>
    </td>
    <td>  
<img width="755" height="875" alt="after"
src="https://github.com/user-attachments/assets/dca31af3-e571-445a-b4a9-c300bb4c63fa"
/>
    </td>
  </tr>
</table>
2025-12-19 08:43:35 +00:00
Mustaque Ahmed
e44529ed7b Hide inline overlays when context menu is open (#45266)
Closes #23367 

**Summary**
- Prevents inline diagnostics, code actions, blame annotations, and
hover popovers from overlapping with the right-click context menu by
checking for `mouse_context_menu` presence before rendering these UI
elements.

PS: Same behaviour is present in other editors like VS Code.


**Screen recording**


https://github.com/user-attachments/assets/8290412b-0f86-4985-8c70-13440686e530



Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-19 08:24:30 +00:00
rabsef-bicrym
e052127e1c terminal: Prevent scrollbar arithmetic underflow panic (#45282)
## Summary

Fixes arithmetic underflow panics in `terminal_scrollbar.rs` by
converting unsafe subtractions to `saturating_sub`.

Closes #45281

## Problem

Two locations perform raw subtraction on `usize` values that panic when
underflow occurs:

- `offset()`: `state.total_lines - state.viewport_lines -
state.display_offset`
- `set_offset()`: `state.total_lines - state.viewport_lines`

This happens when `total_lines < viewport_lines + display_offset`, which
can occur during terminal creation, with small window sizes, or when
display state becomes stale.

## Solution

Replace the two unsafe subtractions with `saturating_sub`, which returns
0 on underflow instead of panicking.

Also standardizes the existing `checked_sub().unwrap_or(0)` in
`max_offset()` to `saturating_sub` for consistency across the file.

## Changes

- N/A
2025-12-19 06:33:59 +00:00
Ryan Steil
0531035b86 docs: Fix link to Anthropic prompt engineering resource (#45329) 2025-12-19 01:47:40 -03:00
Ben Kunkle
05ce34eea4 ci: Fix docs build post #45130 (#45330)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-19 03:40:27 +00:00
Alvaro Parker
63c4406137 git: Add git clone open listener (#41669) 2025-12-19 00:21:46 -03:00
Ben Kunkle
3f67c5220d Remove zed dependency from docs_preprocessor (#45130)
Closes #ISSUE

Uses the existing `--dump-all-actions` arg on the Zed binary to generate
an asset of all of our actions so that the `docs_preprocessor` can
injest it, rather than depending on the Zed crate itself to collect all
action names

Release Notes:

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

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
2025-12-18 21:59:05 -05:00
Ben Kunkle
435d4c5f24 vim: Make vaf include const for arrow functions in JS/TS/TSX (#45327)
Closes #24264

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-18 21:56:47 -05:00
Danilo Leal
e0ff995e2d agent ui: Make some UI elements more consistent (#45319)
- Both the mode, profile, and model selectors have the option to cycle
through its options with a keybinding. In the tooltip that shows it, in
some of them the "Cycle Through..." label was at the top, and in others
at the bottom. Now it's all at the bottom.
- We used different language in different places for "going to a file".
The tool call edit card's header said "_Jump_ to File" while the edit
files list said "_Go_ to File". Now it's both "Go to File".

Release Notes:

- N/A
2025-12-19 01:18:41 +00:00
Conrad Irwin
6976208e21 Move autofix stuff to zippy (#45304)
Although I wanted to avoid the dependency, it's hard to get github to do
what we want.

Release Notes:

- N/A
2025-12-18 15:23:09 -07:00
Richard Feldman
6055b45ee1 Add support for provider extensions (but no extensions yet) (#45277)
This adds support for provider extensions but doesn't actually add any
yet.

Release Notes:

- N/A
2025-12-18 17:05:04 -05:00
Joseph T. Lyons
88f90c12ed Add language server version in a tooltip on language server hover (#45302)
I wanted a way to make it easy to figure out which version of a language
server Zed is running. Now, you get a tooltip when hovering on a
language server in the Language Servers popover.

<img width="498" height="168" alt="SCR-20251218-ovln"
src="https://github.com/user-attachments/assets/1ced4214-b868-4405-8881-eb7c0b75a53e"
/>

This PR also fixes a bug. We had existing code to open a tooltip on
these language server entrees and display the language server message,
which was never fully wired up for `CustomEntry`s. Now, in this PR, we
will show show either version, message, or both, in the documentation
aside, depending on what the server has given us.

Mostly done with Droid (using GPT-5.2), with manual review and multiple
follow ups to guide it into using existing patterns in the codebase,
when it did something abnormal.

Release Notes:

- Added language server version in a tooltip on language server hover

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-18 21:59:21 +00:00
Marshall Bowers
0d74f982a5 danger: Upgrade danger-plugin-pr-hygiene to v0.7.1 (#45303)
This PR upgrades `danger-plugin-pr-hygiene` to v0.7.1.

Release Notes:

- N/A
2025-12-18 21:52:34 +00:00
Marshall Bowers
ca90b8555d docs: Remove local collaboration docs (#45301)
This PR removes the docs for running Collab locally, as they are
outdated and don't reflect the current state of affairs.

Release Notes:

- N/A
2025-12-18 21:42:28 +00:00
Richard Feldman
8516d81e13 Fix display name for Ollama models (#45287)
Closes #43646

Release Notes:

- Fixed display name for Ollama models
2025-12-18 16:32:59 -05:00
Danilo Leal
af589ff25f agent_ui: Simplify timestamp display (#45296)
This PR simplifies how we display thread timestamps in the agent panel's
history view. For threads that are older-than-yesterday, we just show
how many days ago that thread was had in. Hovering over the thread item
shows you both the title and the full date, if needed (time and date).

<img width="450" height="786" alt="Screenshot 2025-12-18 at 5  24@2x"
src="https://github.com/user-attachments/assets/11416e9b-f1b0-4307-9db0-988a95a316a1"
/>


Release Notes:

- N/A
2025-12-18 17:49:17 -03:00
Julia Ryan
d2bbfbb3bf lsp: Broadcast our capability for MessageActionItems (#45047)
Closes #37902

Release Notes:

- Enable LSP Message action items for more language servers. These are interactive prompts, often for things like downloading build inputs for a project.
2025-12-18 11:09:40 -08:00
Peter Tripp
413f4ea49c Redact environment variables from language server spawn errors (#44783)
Redact environment variables from zed logs when lsp fails to spawn.

Release Notes:

- N/A
2025-12-18 21:05:14 +02:00
Marshall Bowers
1b6d588413 danger: Deny conventional commits in PR titles (#45283)
This PR upgrades `danger-plugin-pr-hygiene` to v0.7.0 so that we can
have Danger deny conventional commits in PR titles.

Release Notes:

- N/A
2025-12-18 18:42:28 +00:00
Gaauwe Rombouts
334ca21857 Truncate code actions with a long label and show full label aside (#45268)
Closes #43355

Fixes the issue were code actions with long labels would get cut off
without being able to see the full description. We now properly truncate
those labels with an ellipsis and show the full description in an aside.

Release Notes:

- Added ellipsis to truncated code actions and an aside showing the full
action description.
2025-12-18 18:05:53 +01:00
Emmanuel Amoah
f58278aaf4 glossary: Fix grammar and typo (#45267)
Fixes grammar and a typo in `Picker` description.

Release Notes:

- N/A
2025-12-18 17:03:46 +00:00
Leo
e10b9b70ef git: Add global git integration enable/disable setting (#43326)
Closes #13304

Release Notes:

- Add global `git status` and `git diff` on/off in one place instead of
control everywhere

We can first review to ensure this change meets both `Zed` and user
requirements, as well as code rules. Currently, we only support
user-level settings. We can wait for this PR:
https://github.com/zed-industries/zed/pull/43173 to be merged, then
modify it to support both user and project levels.
2025-12-18 11:45:26 -05:00
Marco Mihai Condrache
098adf3bdd gpui: Enable direct-to-display optimization for metal (#44334)
When profiling Zed with Instruments, a warning appears indicating that
surfaces cannot be pushed directly to the display as they are
non-opaque. This happens because the metal layer is currently marked as
non-opaque by default, even though the window itself is not transparent.

<img width="590" height="55" alt="image"
src="https://github.com/user-attachments/assets/2647733e-c75b-4aec-aa19-e8b2ffd6194b"
/>

Metal on macOS can bypass compositing and present frames directly to the
display when several conditions are met. One of those conditions is that
the backing layer must be declared opaque. Apple’s documentation notes
that marking layers as opaque allows the system to avoid unnecessary
compositing work, reducing GPU load and improving frame pacing

Ref:
https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos

This PR updates the Metal renderer to mark the layer as opaque whenever
the window does not use transparency. This makes Zed eligible for
macOS’s direct-to-display optimization in scenarios where the system can
apply it.

Release Notes:

- gpui: Mark metal layers opaque for non-transparent windows to allow
direct-to-display when supported

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-12-18 11:29:25 -05:00
Jakub Konka
a85c508f69 Fix self-referential symbolic link (#45265)
Release Notes:

- N/A
2025-12-18 17:26:20 +01:00
tidely
2a713c546b gpui: Small tab group performance improvements (#41885)
Closes #ISSUE

Removes a few eager container clones and iterations.

Added a todo to `get_prev_tab_group_window` and
`get_next_tab_group_window`. They seem to use `HashMap::keys()` for
choosing the previous tab group, however `.keys()` returns an arbitrary
order, so I'm not sure if previous actually means anything here. Conrad
seems to have worked on this part previously, maybe he has some
insights. That can possibly be a follow-up PR, but I'd be willing to
work on it here as well since the other changes are so simple.

Release Notes:

- N/A
2025-12-18 11:24:38 -05:00
Bennet Bo Fenner
f937c1931f rules_library: Only store built-in prompts when they are customized (#45112)
Follow up to #45004

Release Notes:

- N/A
2025-12-18 17:21:41 +01:00
Danilo Leal
7a62f01ea5 agent_ui: Use display name for the message editor placeholder (#45264)
Follow up to a regression that happened when we introduced agent servers
that made everywhere displaying agent names use the extension name
instead of the display name. This has been since fixed in other places
and this PR now updates the agent panel's message editor, too:

| Before | After |
|--------|--------|
| <img width="1154" height="254" alt="Screenshot 2025-12-18 at 12  54
2@2x"
src="https://github.com/user-attachments/assets/5f3de9f9-4e11-42f6-90c2-56fc8cdff32e"
/> | <img width="1154" height="254" alt="Screenshot 2025-12-18 at 12 
54@2x"
src="https://github.com/user-attachments/assets/46ed5c45-7e1d-4cc6-b219-b6cc19206d1b"
/> |

Release Notes:

- N/A
2025-12-18 13:08:46 -03:00
Sean Hagstrom
2d071b0cb6 editor: Fix git-hunk toggling for adjacent hunks (#43187)
Closes #42934 

Release Notes:

- Fix toggling adjacent git-diff hunks based on the reported behaviour
in #42934

---------

Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
2025-12-18 16:45:55 +01:00
Alvaro Parker
bd2b0de231 gpui: Add modal dialog window kind (#40291)
Closes #ISSUE

A [modal dialog](https://en.wikipedia.org/wiki/Modal_window) window is a
window that demands the user's immediate attention and blocks
interaction with other parts of the application until it's closed.

- On Windows this is done by disabling the parent window when the dialog
window is created and re-enabling the parent window when closed.
- On Wayland this is done using the
[`XdgDialog`](https://wayland.app/protocols/xdg-dialog-v1) protocol,
which hints to the compositor that the dialog should be modal. While
compositors like GNOME and KDE block parent interaction automatically,
the XDG specification does not guarantee this behavior, compositors may
deliver events to the parent window unfiltered. Since the specification
explicitly requires clients to implement event filtering logic
themselves, this PR implements client-side blocking in GPUI to ensure
consistent modal behavior across all Wayland compositors, including
those like Hyprland that don't block parent interaction.
- On X11 this is done by enabling the application window property
[`_NET_WM_STATE_MODAL`](https://specifications.freedesktop.org/wm/latest/ar01s05.html#id-1.6.8)
state.

I'm unable to implement this on MacOS as I lack the experience and the
hardware to test it. If anyone is interested on implementing this let me
know.

|Window|Linux (wayland)| Linux (x11) |MacOS|
|-|-|-|-|
|<video
src="https://github.com/user-attachments/assets/bfd0733a-445d-4b63-ac6b-ebe098a7dc74"></video>|<video
src="https://github.com/user-attachments/assets/024cd6ec-ff81-4250-a5be-5d207a023f8c"></video>|
N/A | <video
src="https://github.com/user-attachments/assets/656e60a5-26b2-4ee2-8368-1fbbe872453c"></video>|

TODO:

- [x] Block parent interaction client-side on X11

Release Notes:

- Added modal dialog window kind on GPUI

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
2025-12-18 16:45:06 +01:00
Bennet Bo Fenner
886de8f54b agent_ui: Improve UX when pasting code into message editor (#45254)
Follow up to #42982

Release Notes:

- agent: Allow pasting code without formatting via ctrl/cmd-shift-v.
- agent: Fixed an issue where pasting a single line of code would always
insert an @mention
2025-12-18 16:38:47 +01:00
Ben Brandt
7a783a91cc acp: Update to agent-client-protocol rust sdk v0.9.2 (#45255)
Release Notes:

- N/A
2025-12-18 15:01:20 +00:00
Ahmed M. Ammar
f9462da2f7 terminal: Fix pane re-entrancy panic when splitting terminal tabs (#45231)
## Summary
Fix panic "cannot update workspace::pane::Pane while it is already being
updated" when dragging terminal tabs to split the pane.

## Problem
When dragging a terminal tab to create a split, the app panics due to
re-entrancy: the drop handler calls `terminal_panel.center.split()`
synchronously, which invokes `mark_positions()` that tries to update all
panes in the group. When the pane being updated is part of the terminal
panel's center group, this causes a re-entrancy panic.

## Solution
Defer the split operation using `cx.spawn_in()`, similar to how
`move_item` was already deferred in the same handler. This ensures the
split (and subsequent `mark_positions()` call) runs after the current
pane update completes.

## Test plan
- Open terminal panel
- Create a terminal tab
- Drag the terminal tab to split the pane
- Verify no panic occurs and split works correctly
2025-12-18 14:34:33 +00:00
Danilo Leal
61dd6a8f31 agent_ui: Add some fixes to tool calling display (#45252)
- Follow up to https://github.com/zed-industries/zed/pull/45097 — not
showing raw inputs for edit and terminal calls
- Removing the display of empty Markdown if the model outputs it

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2025-12-18 14:34:10 +00:00
Ben Brandt
abb199c85e thread_view: Clearer authentication states (#45230)
Closes #44717

Sometimes, we show the user the agent's auth methods because we got an
AuthRequired error.

However, there are also several ways a user can choose to re-enter the
authentication flow even though they are still logged in.

This has caused some confusion with several users, where after logging
in, they type /login again to see if anything changed, and they saw an
"Authentication Required" warning.

So, I made a distinction in the UI if we go to this flow from a concrete
error, or if not, made the language less error-like to help avoid
confusion.

| Before | After |
|--------|--------|
| <img width="1154" height="446" alt="Screenshot 2025-12-18 at 10 
54@2x"
src="https://github.com/user-attachments/assets/9df0d59a-2d45-4bfc-ba85-359dd1a4c8ae"
/> | <img width="1154" height="446" alt="Screenshot 2025-12-18 at 10 
53@2x"
src="https://github.com/user-attachments/assets/73a9fb45-4e6f-4594-8795-aaade35b2a72"
/> |


Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Miguel Raz Guzmán Macedo <miguel@zed.dev>
2025-12-18 14:03:11 +00:00
Lukas Wirth
cebbf77491 gpui(windows): Fix clicks to inactive windows not dispatching to the clicked window (#45237)
Release Notes:

- Fixed an issue on windows where clicking buttons on windows in the
background would not register as being clicked on that window
2025-12-18 13:05:20 +00:00
Ben Brandt
0180f3e72a deepseek: Fix for max output tokens blocking completions (#45236)
They count the requested max_output_tokens against the prompt total.
Seems like a bug on their end as most other providers don't do this, but
now we just default to None for the main models and let the API use its
default behavior which works just fine.

Closes: #45134

Release Notes:

- deepseek: Fix issue with Deepseek API that was causing the token limit
to be reached sooner than necessary
2025-12-18 12:47:34 +00:00
rabsef-bicrym
5488a19221 terminal: Respect RevealStrategy::NoFocus and Never focus settings (#45180)
Closes #45179

## Summary

Fixes the focus behavior when creating terminals with
`RevealStrategy::NoFocus` or `RevealStrategy::Never`. Previously,
terminals would still receive focus if the terminal pane already had
focus, contradicting the documented behavior.

## Changes

- **`add_terminal_task()`**: Changed focus logic to only focus when
`RevealStrategy::Always`
- **`add_terminal_shell()`**: Same fix

The fix changes:
```rust
// Before
let focus = pane.has_focus(window, cx)
    || matches!(reveal_strategy, RevealStrategy::Always);

// After  
let focus = matches!(reveal_strategy, RevealStrategy::Always);
```

## Impact

This affects:
- Vim users running `:!command` (uses `NoFocus`)
- Debugger terminal spawning (uses `NoFocus`)
- Any programmatic terminal creation requesting background behavior

Release Notes:

- Fixed terminal focus behavior to respect `RevealStrategy::NoFocus` and
`RevealStrategy::Never` settings when the terminal pane already has
focus.
2025-12-18 12:11:14 +00:00
Henry Chu
bb1198e7d6 languages: Allow using locally installed ty for Python (#45193)
Release Notes:

- Allow using locally installed `ty` for Python
2025-12-18 12:54:34 +01:00
Kirill Bulatov
69fe27f45e Keep tab stop-less snippets in completion list (#45227)
Closes https://github.com/zed-industries/zed/issues/45083

cc @agu-z 

Release Notes:

- Fixed certain rust-analyzer snippets not shown
2025-12-18 11:29:41 +00:00
Lukas Wirth
469da2fd07 gpui: Fix Windows credential lookup returning error instead of None when credentials don't exist (#45228)
This spams the log with amazon bedrock otherwise

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-18 11:23:11 +00:00
shibang
4f87822133 gpui: Persist window bounds and display when detaching a workspace session (#45201)
Closes #41246 #45092

Release Notes:

- N/A

**Root Cause**:
Empty local workspaces returned `DetachFromSession` from 
`serialize_workspace_location()`, and the `DetachFromSession` handler
only cleared the session_id **without saving window bounds**.

**Fix Applied**:
Modified the `DetachFromSession` handler to save window bounds via
`set_window_open_status()`:
```rust
WorkspaceLocation::DetachFromSession => {
    let window_bounds = SerializedWindowBounds(window.window_bounds());
    let display = window.display(cx).and_then(|d| d.uuid().ok());
    window.spawn(cx, async move |_| {
        persistence::DB
            .set_window_open_status(database_id, window_bounds, display.unwrap_or_default())
            .await.log_err();
        persistence::DB.set_session_id(database_id, None).await.log_err();
    })
}
```

**Recording**:


https://github.com/user-attachments/assets/2b6564d4-4e1b-40fe-943b-147296340aa7
2025-12-18 12:03:42 +01:00
Ben Brandt
9a69d89f88 thread_view: Remove unused acp auth method (#45221)
This was from an early iteration and this code path isn't used anymore

Release Notes:

- N/A
2025-12-18 10:47:36 +00:00
Kirill Bulatov
54f360ace1 Add a test to ensure we invalidate brackets not only on edits (#45219)
Follow-up of https://github.com/zed-industries/zed/pull/45187

Release Notes:

- N/A
2025-12-18 10:42:37 +00:00
Ben Brandt
b2a0b78ece acp: Change default for gemini back to managed version (#45218)
It seems we unintentionally changed the default behavior of if we use
the gemini on the path in #40663

Changing this back so by default we use a managed version of the CLI so
we can better control min versions and the like, but still allow people
to override if they need to.

Release Notes:

- N/A
2025-12-18 10:25:06 +00:00
MostlyK
f1ca2f9f31 workspace: Fix new projects opening with default window size (#45204)
Previously, when opening a new project (one that was never opened
before), the window bounds restoration logic would fall through to
GPUI's default window sizing instead of using the last known window
bounds.

This change consolidates the window bounds restoration logic so that
both empty workspaces and new projects use the stored default window
bounds, making the behavior consistent: any new window will use the last
resized window's size and position.

Closes #45092 

Release Notes:

- Fixed new files and projects opening with default window size instead
of the last used window size.
2025-12-18 09:57:21 +00:00
Guilherme do Amaral Alves
4b34adedd2 Update Mistral models context length to their recommended values (#45194)
I noticed some of mistral models context lenghts were outdated, they
were updated accordingly to mistral documentation.

The following models had their context lenght changed:

[mistral-large-latest](https://docs.mistral.ai/models/mistral-large-3-25-12)

[magistral-medium-latest](https://docs.mistral.ai/models/magistral-medium-1-2-25-09)

[magistral-small-latest](https://docs.mistral.ai/models/magistral-small-1-2-25-09)

[devstral-medium-latest](https://docs.mistral.ai/models/devstral-2-25-12)

[devstral-small-latest](https://docs.mistral.ai/models/devstral-small-2-25-12)
2025-12-18 09:49:32 +00:00
Oleksii Orlenko
df48294caa agent_ui: Remove unnecessary Arc allocation (#45172)
Follow up to https://github.com/zed-industries/zed/pull/44297.

Initial implementation in ce884443f1 used
`Arc` to store the reference to the hash map inside the iterator while
keeping the lifetime static. The code was later simplified in
5151b22e2e to build the list eagerly but
the Arc was forgotten, although it became unnecessary.

cc @bennetbo

Release Notes:

- N/A
2025-12-18 10:48:45 +01:00
Kirill Bulatov
cdc5cc348f Return back the eager snapshot update (#45210)
Based on
https://github.com/zed-industries/zed/pull/45187#discussion_r2630140112

Release Notes:

- N/A

Co-authored-by: Lukas Wirth <lukas@zed.dev>
2025-12-18 09:32:35 +00:00
Kirill Bulatov
0f7f540138 Always invalidate tree-sitter data on buffer reparse end (#45187)
Also do not eagerly invalidate this data on buffer reparse start

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

Release Notes:

- Fixed bracket colorization not applied on initial file open
2025-12-18 02:37:26 +00:00
Kunall Banerjee
184001b33b docs: Add note about conflicting global macOS shortcut (#45186)
This is already noted in our `default-macos.json`, but was never
surfaced in our docs for some reason. A user noted their LSP completions
were not working because they were not aware of the conflicting global
shortcut.

Ref:
https://github.com/zed-industries/zed/issues/44970#issuecomment-3664118523

Release Notes:

- N/A
2025-12-18 02:13:59 +00:00
Xiaobo Liu
225a2a8a20 google_ai: Refactor token count methods in Google AI (#45184)
The change simplifies the `max_token_count` and `max_output_tokens`
methods by grouping Gemini models with identical token limits.

Release Notes:

- N/A
2025-12-17 20:12:40 -06:00
Kirill Bulatov
ea37057814 Restore generic modal closing on mouse click (#45183)
Was removed in
https://github.com/zed-industries/zed/pull/44887/changes#diff-1de872be76a27a9d574a0b0acec4581797446e60743d23b3e7a5f15088fa7e61

Release Notes:

- (Preview only) Fixed certain modals not being dismissed on mouse click
outside
2025-12-18 01:56:12 +00:00
Conrad Irwin
77cdef3596 Attempt to fix the autofix auto scheduler (#45178)
Release Notes:

- N/A
2025-12-18 01:04:12 +00:00
Torstein Sørnes
05108c50fd agent_ui: Make tool call raw input visible (#45097)
<img width="500" height="1246" alt="Screenshot 2025-12-17 at 9  28@2x"
src="https://github.com/user-attachments/assets/eddb290d-d4d0-4ab8-94b3-bcc50ad07157"
/>

Release Notes:

- agent: Made tool calls' raw input visible in the agent UI.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-12-18 00:34:31 +00:00
Ben Kunkle
07538ff08e Make sweep and mercury API tokens use cx.global instead of OnceLock (#45176)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-12-18 00:32:46 +00:00
Cole Miller
9073a2666c Revert "git: Mark entries as pending when staging a files making the staged highlighting more "optimistic"" (#45175)
Reverts zed-industries/zed#43434

This caused a regression because the additional pending hunks don't get
cleared.
2025-12-18 00:28:09 +00:00
Max Brunsfeld
843a35a1a9 extension api: Make server id types constructible, to ease writing tests (#45174)
Currently, extensions cannot have tests that call methods like
`label_for_symbol` and `label_for_completion`, because those methods
take a `LanguageServerId`, and that type is opaque, and cannot be
constructed outside of the `zed_extension_api` crate.

This PR makes it possible to construct those types from strings, so that
it's more straightforward to write unit tests for these LSP adapter
methods.

Release Notes:

- N/A
2025-12-17 16:25:07 -08:00
Conrad Irwin
aff93f2f6c More permissions for autofix (#45170)
Release Notes:

- N/A
2025-12-17 17:05:35 -07:00
374 changed files with 21279 additions and 8044 deletions

View File

@@ -0,0 +1,55 @@
# Phase 2: Explore Repository
You are analyzing a codebase to understand its structure before reviewing documentation impact.
## Objective
Produce a structured overview of the repository to inform subsequent documentation analysis.
## Instructions
1. **Identify Primary Languages and Frameworks**
- Scan for Cargo.toml, package.json, or other manifest files
- Note the primary language(s) and key dependencies
2. **Map Documentation Structure**
- This project uses **mdBook** (https://rust-lang.github.io/mdBook/)
- Documentation is in `docs/src/`
- Table of contents: `docs/src/SUMMARY.md` (mdBook format: https://rust-lang.github.io/mdBook/format/summary.html)
- Style guide: `docs/.rules`
- Agent guidelines: `docs/AGENTS.md`
- Formatting: Prettier (config in `docs/.prettierrc`)
3. **Identify Build and Tooling**
- Note build systems (cargo, npm, etc.)
- Identify documentation tooling (mdbook, etc.)
4. **Output Format**
Produce a JSON summary:
```json
{
"primary_language": "Rust",
"frameworks": ["GPUI"],
"documentation": {
"system": "mdBook",
"location": "docs/src/",
"toc_file": "docs/src/SUMMARY.md",
"toc_format": "https://rust-lang.github.io/mdBook/format/summary.html",
"style_guide": "docs/.rules",
"agent_guidelines": "docs/AGENTS.md",
"formatter": "prettier",
"formatter_config": "docs/.prettierrc",
"custom_preprocessor": "docs_preprocessor (handles {#kb action::Name} syntax)"
},
"key_directories": {
"source": "crates/",
"docs": "docs/src/",
"extensions": "extensions/"
}
}
```
## Constraints
- Read-only: Do not modify any files
- Focus on structure, not content details
- Complete within 2 minutes

View File

@@ -0,0 +1,57 @@
# Phase 3: Analyze Changes
You are analyzing code changes to understand their nature and scope.
## Objective
Produce a clear, neutral summary of what changed in the codebase.
## Input
You will receive:
- List of changed files from the triggering commit/PR
- Repository structure from Phase 2
## Instructions
1. **Categorize Changed Files**
- Source code (which crates/modules)
- Configuration
- Tests
- Documentation (already existing)
- Other
2. **Analyze Each Change**
- Review diffs for files likely to impact documentation
- Focus on: public APIs, settings, keybindings, commands, user-visible behavior
3. **Identify What Did NOT Change**
- Note stable interfaces or behaviors
- Important for avoiding unnecessary documentation updates
4. **Output Format**
Produce a markdown summary:
```markdown
## Change Analysis
### Changed Files Summary
| Category | Files | Impact Level |
| --- | --- | --- |
| Source - [crate] | file1.rs, file2.rs | High/Medium/Low |
| Settings | settings.json | Medium |
| Tests | test_*.rs | None |
### Behavioral Changes
- **[Feature/Area]**: Description of what changed from user perspective
- **[Feature/Area]**: Description...
### Unchanged Areas
- [Area]: Confirmed no changes to [specific behavior]
### Files Requiring Deeper Review
- `path/to/file.rs`: Reason for deeper review
```
## Constraints
- Read-only: Do not modify any files
- Neutral tone: Describe what changed, not whether it's good/bad
- Do not propose documentation changes yet

View File

@@ -0,0 +1,76 @@
# Phase 4: Plan Documentation Impact
You are determining whether and how documentation should be updated based on code changes.
## Objective
Produce a structured documentation plan that will guide Phase 5 execution.
## Documentation System
This is an **mdBook** site (https://rust-lang.github.io/mdBook/):
- `docs/src/SUMMARY.md` defines book structure per https://rust-lang.github.io/mdBook/format/summary.html
- If adding new pages, they MUST be added to SUMMARY.md
- Use `{#kb action::ActionName}` syntax for keybindings (custom preprocessor expands these)
- Prettier formatting (80 char width) will be applied automatically
## Input
You will receive:
- Change analysis from Phase 3
- Repository structure from Phase 2
- Documentation guidelines from `docs/AGENTS.md`
## Instructions
1. **Review AGENTS.md**
- Load and apply all rules from `docs/AGENTS.md`
- Respect scope boundaries (in-scope vs out-of-scope)
2. **Evaluate Documentation Impact**
For each behavioral change from Phase 3:
- Does existing documentation cover this area?
- Is the documentation now inaccurate or incomplete?
- Classify per AGENTS.md "Change Classification" section
3. **Identify Specific Updates**
For each required update:
- Exact file path
- Specific section or heading
- Type of change (update existing, add new, deprecate)
- Description of the change
4. **Flag Uncertainty**
Explicitly mark:
- Assumptions you're making
- Areas where human confirmation is needed
- Ambiguous requirements
5. **Output Format**
Use the exact format specified in `docs/AGENTS.md` Phase 4 section:
```markdown
## Documentation Impact Assessment
### Summary
Brief description of code changes analyzed.
### Documentation Updates Required: [Yes/No]
### Planned Changes
#### 1. [File Path]
- **Section**: [Section name or "New section"]
- **Change Type**: [Update/Add/Deprecate]
- **Reason**: Why this change is needed
- **Description**: What will be added/modified
### Uncertainty Flags
- [ ] [Description of any assumptions or areas needing confirmation]
### No Changes Needed
- [List files reviewed but not requiring updates, with brief reason]
```
## Constraints
- Read-only: Do not modify any files
- Conservative: When uncertain, flag for human review rather than planning changes
- Scoped: Only plan changes that trace directly to code changes from Phase 3
- No scope expansion: Do not plan "improvements" unrelated to triggering changes

View File

@@ -0,0 +1,67 @@
# Phase 5: Apply Documentation Plan
You are executing a pre-approved documentation plan for an **mdBook** documentation site.
## Objective
Implement exactly the changes specified in the documentation plan from Phase 4.
## Documentation System
- **mdBook**: https://rust-lang.github.io/mdBook/
- **SUMMARY.md**: Follows mdBook format (https://rust-lang.github.io/mdBook/format/summary.html)
- **Prettier**: Will be run automatically after this phase (80 char line width)
- **Custom preprocessor**: Use `{#kb action::ActionName}` for keybindings instead of hardcoding
## Input
You will receive:
- Documentation plan from Phase 4
- Documentation guidelines from `docs/AGENTS.md`
- Style rules from `docs/.rules`
## Instructions
1. **Validate Plan**
- Confirm all planned files are within scope per AGENTS.md
- Verify no out-of-scope files are targeted
2. **Execute Each Planned Change**
For each item in "Planned Changes":
- Navigate to the specified file
- Locate the specified section
- Apply the described change
- Follow style rules from `docs/.rules`
3. **Style Compliance**
Every edit must follow `docs/.rules`:
- Second person, present tense
- No hedging words ("simply", "just", "easily")
- Proper keybinding format (`Cmd+Shift+P`)
- Settings Editor first, JSON second
- Correct terminology (folder not directory, etc.)
4. **Preserve Context**
- Maintain surrounding content structure
- Keep consistent heading levels
- Preserve existing cross-references
## Constraints
- Execute ONLY changes listed in the plan
- Do not discover new documentation targets
- Do not make stylistic improvements outside planned sections
- Do not expand scope beyond what Phase 4 specified
- If a planned change cannot be applied (file missing, section not found), skip and note it
## Output
After applying changes, output a summary:
```markdown
## Applied Changes
### Successfully Applied
- `path/to/file.md`: [Brief description of change]
### Skipped (Could Not Apply)
- `path/to/file.md`: [Reason - e.g., "Section not found"]
### Warnings
- [Any issues encountered during application]
```

View File

@@ -0,0 +1,54 @@
# Phase 6: Summarize Changes
You are generating a summary of documentation updates for PR review.
## Objective
Create a clear, reviewable summary of all documentation changes made.
## Input
You will receive:
- Applied changes report from Phase 5
- Original change analysis from Phase 3
- Git diff of documentation changes
## Instructions
1. **Gather Change Information**
- List all modified documentation files
- Identify the corresponding code changes that triggered each update
2. **Generate Summary**
Use the format specified in `docs/AGENTS.md` Phase 6 section:
```markdown
## Documentation Update Summary
### Changes Made
| File | Change | Related Code |
| --- | --- | --- |
| docs/src/path.md | Brief description | PR #123 or commit SHA |
### Rationale
Brief explanation of why these updates were made, linking back to the triggering code changes.
### Review Notes
- Items reviewers should pay special attention to
- Any uncertainty flags from Phase 4 that were addressed
- Assumptions made during documentation
```
3. **Add Context for Reviewers**
- Highlight any changes that might be controversial
- Note if any planned changes were skipped and why
- Flag areas where reviewer expertise is especially needed
## Output Format
The summary should be suitable for:
- PR description body
- Commit message (condensed version)
- Team communication
## Constraints
- Read-only (documentation changes already applied in Phase 5)
- Factual: Describe what was done, not justify why it's good
- Complete: Account for all changes, including skipped items

View File

@@ -0,0 +1,67 @@
# Phase 7: Commit and Open PR
You are creating a git branch, committing documentation changes, and opening a PR.
## Objective
Package documentation updates into a reviewable pull request.
## Input
You will receive:
- Summary from Phase 6
- List of modified files
## Instructions
1. **Create Branch**
```sh
git checkout -b docs/auto-update-{date}
```
Use format: `docs/auto-update-YYYY-MM-DD` or `docs/auto-update-{short-sha}`
2. **Stage and Commit**
- Stage only documentation files in `docs/src/`
- Do not stage any other files
Commit message format:
```
docs: auto-update documentation for [brief description]
[Summary from Phase 6, condensed]
Triggered by: [commit SHA or PR reference]
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
```
3. **Push Branch**
```sh
git push -u origin docs/auto-update-{date}
```
4. **Create Pull Request**
Use the Phase 6 summary as the PR body.
PR Title: `docs: [Brief description of documentation updates]`
Labels (if available): `documentation`, `automated`
Base branch: `main`
## Constraints
- Do NOT auto-merge
- Do NOT request specific reviewers (let CODEOWNERS handle it)
- Do NOT modify files outside `docs/src/`
- If no changes to commit, exit gracefully with message "No documentation changes to commit"
## Output
```markdown
## PR Created
- **Branch**: docs/auto-update-{date}
- **PR URL**: https://github.com/zed-industries/zed/pull/XXXX
- **Status**: Ready for review
### Commit
- SHA: {commit-sha}
- Files: {count} documentation files modified
```

View File

@@ -25,6 +25,7 @@ self-hosted-runner:
- namespace-profile-32x64-ubuntu-2204
# Namespace Ubuntu 24.04 (like ubuntu-latest)
- namespace-profile-2x4-ubuntu-2404
- namespace-profile-8x32-ubuntu-2404
# Namespace Limited Preview
- namespace-profile-8x16-ubuntu-2004-arm-m4
- namespace-profile-8x32-ubuntu-2004-arm-m4

View File

@@ -19,6 +19,18 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- name: Install mold linker
shell: bash -euxo pipefail {0}
run: ./script/install-mold
- name: Download WASI SDK
shell: bash -euxo pipefail {0}
run: ./script/download-wasi-sdk
- name: Generate action metadata
shell: bash -euxo pipefail {0}
run: ./script/generate-action-metadata
- name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:

View File

@@ -54,6 +54,10 @@ jobs:
- name: autofix_pr::run_autofix::run_cargo_fmt
run: cargo fmt --all
shell: bash -euxo pipefail {0}
- name: autofix_pr::run_autofix::run_cargo_fix
if: ${{ inputs.run_clippy }}
run: cargo fix --workspace --release --all-targets --all-features --allow-dirty --allow-staged
shell: bash -euxo pipefail {0}
- name: autofix_pr::run_autofix::run_clippy_fix
if: ${{ inputs.run_clippy }}
run: cargo clippy --workspace --release --all-targets --all-features --fix --allow-dirty --allow-staged

View File

@@ -33,18 +33,19 @@ jobs:
bobbymannino
CharlesChen0823
chbk
cppcoffee
davidbarsky
davewa
davidbarsky
ddoemonn
djsauble
errmayank
fantacell
fdncred
findrakecil
FloppyDisco
gko
huacnlee
imumesh18
injust
jacobtread
jansol
jeffreyguenther
@@ -56,21 +57,23 @@ jobs:
marius851000
mikebronner
ognevny
PKief
playdohface
RemcoSmitsDev
romaninsh
rxptr
Simek
someone13574
sourcefrog
suxiaoshao
Takk8IS
tartarughina
thedadams
tidely
timvermeulen
valentinegb
versecafe
vitallium
warrenjokinen
WhySoBad
ya7010
Zertsov

View File

@@ -1,29 +1,40 @@
name: "Close Stale Issues"
on:
schedule:
- cron: "0 8 31 DEC *"
- cron: "0 2 * * 5"
workflow_dispatch:
inputs:
debug-only:
description: "Run in dry-run mode (no changes made)"
type: boolean
default: false
operations-per-run:
description: "Max number of issues to process (default: 1000)"
type: number
default: 1000
jobs:
stale:
if: github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: >
Hi there! 👋
We're working to clean up our issue tracker by closing older bugs that might not be relevant anymore. If you are able to reproduce this issue in the latest version of Zed, please let us know by commenting on this issue, and it will be kept open. If you can't reproduce it, feel free to close the issue yourself. Otherwise, it will close automatically in 14 days.
Hi there!
Zed development moves fast and a significant number of bugs become outdated.
If you can reproduce this bug on the latest stable Zed, please let us know by leaving a comment with the Zed version.
If the bug doesn't appear for you anymore, feel free to close the issue yourself; otherwise, the bot will close it in a couple of weeks.
Thanks for your help!
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please open a new issue with a link to this issue."
close-issue-message: "This issue was closed due to inactivity. If you're still experiencing this problem, please leave a comment with your Zed version so that we can reopen the issue."
days-before-stale: 60
days-before-close: 14
only-issue-types: "Bug,Crash"
operations-per-run: 1000
operations-per-run: ${{ inputs.operations-per-run || 1000 }}
ascending: true
enable-statistics: true
debug-only: ${{ inputs.debug-only }}
stale-issue-label: "stale"
exempt-issue-labels: "never stale"

264
.github/workflows/docs_automation.yml vendored Normal file
View File

@@ -0,0 +1,264 @@
name: Documentation Automation
on:
# push:
# branches: [main]
# paths:
# - 'crates/**'
# - 'extensions/**'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to analyze (gets full PR diff)'
required: false
type: string
trigger_sha:
description: 'Commit SHA to analyze (ignored if pr_number is set)'
required: false
type: string
permissions:
contents: write
pull-requests: write
env:
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
DROID_MODEL: claude-opus-4-5-20251101
jobs:
docs-automation:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Droid CLI
id: install-droid
run: |
curl -fsSL https://app.factory.ai/cli | sh
echo "${HOME}/.local/bin" >> "$GITHUB_PATH"
echo "DROID_BIN=${HOME}/.local/bin/droid" >> "$GITHUB_ENV"
# Verify installation
"${HOME}/.local/bin/droid" --version
- name: Setup Node.js (for Prettier)
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Prettier
run: npm install -g prettier
- name: Get changed files
id: changed
run: |
if [ -n "${{ inputs.pr_number }}" ]; then
# Get full PR diff
echo "Analyzing PR #${{ inputs.pr_number }}"
echo "source=pr" >> "$GITHUB_OUTPUT"
echo "ref=${{ inputs.pr_number }}" >> "$GITHUB_OUTPUT"
gh pr diff "${{ inputs.pr_number }}" --name-only > /tmp/changed_files.txt
elif [ -n "${{ inputs.trigger_sha }}" ]; then
# Get single commit diff
SHA="${{ inputs.trigger_sha }}"
echo "Analyzing commit $SHA"
echo "source=commit" >> "$GITHUB_OUTPUT"
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt
else
# Default to current commit
SHA="${{ github.sha }}"
echo "Analyzing commit $SHA"
echo "source=commit" >> "$GITHUB_OUTPUT"
echo "ref=$SHA" >> "$GITHUB_OUTPUT"
git diff --name-only "${SHA}^" "$SHA" > /tmp/changed_files.txt || git diff --name-only HEAD~1 HEAD > /tmp/changed_files.txt
fi
echo "Changed files:"
cat /tmp/changed_files.txt
env:
GH_TOKEN: ${{ github.token }}
# Phase 0: Guardrails are loaded via AGENTS.md in each phase
# Phase 2: Explore Repository (Read-Only - default)
- name: "Phase 2: Explore Repository"
id: phase2
run: |
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
-f .factory/prompts/docs-automation/phase2-explore.md \
> /tmp/phase2-output.txt 2>&1 || true
echo "Repository exploration complete"
cat /tmp/phase2-output.txt
# Phase 3: Analyze Changes (Read-Only - default)
- name: "Phase 3: Analyze Changes"
id: phase3
run: |
CHANGED_FILES=$(tr '\n' ' ' < /tmp/changed_files.txt)
echo "Analyzing changes in: $CHANGED_FILES"
# Build prompt with context
cat > /tmp/phase3-prompt.md << 'EOF'
$(cat .factory/prompts/docs-automation/phase3-analyze.md)
## Context
### Changed Files
$CHANGED_FILES
### Phase 2 Output
$(cat /tmp/phase2-output.txt)
EOF
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
"$(cat .factory/prompts/docs-automation/phase3-analyze.md)
Changed files: $CHANGED_FILES" \
> /tmp/phase3-output.md 2>&1 || true
echo "Change analysis complete"
cat /tmp/phase3-output.md
# Phase 4: Plan Documentation Impact (Read-Only - default)
- name: "Phase 4: Plan Documentation Impact"
id: phase4
run: |
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
-f .factory/prompts/docs-automation/phase4-plan.md \
> /tmp/phase4-plan.md 2>&1 || true
echo "Documentation plan complete"
cat /tmp/phase4-plan.md
# Check if updates are required
if grep -q "NO_UPDATES_REQUIRED" /tmp/phase4-plan.md; then
echo "updates_required=false" >> "$GITHUB_OUTPUT"
else
echo "updates_required=true" >> "$GITHUB_OUTPUT"
fi
# Phase 5: Apply Plan (Write-Enabled with --auto medium)
- name: "Phase 5: Apply Documentation Plan"
id: phase5
if: steps.phase4.outputs.updates_required == 'true'
run: |
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
--auto medium \
-f .factory/prompts/docs-automation/phase5-apply.md \
> /tmp/phase5-report.md 2>&1 || true
echo "Documentation updates applied"
cat /tmp/phase5-report.md
# Phase 5b: Format with Prettier
- name: "Phase 5b: Format with Prettier"
id: phase5b
if: steps.phase4.outputs.updates_required == 'true'
run: |
echo "Formatting documentation with Prettier..."
cd docs && prettier --write src/
echo "Verifying Prettier formatting passes..."
cd docs && prettier --check src/
echo "Prettier formatting complete"
# Phase 6: Summarize Changes (Read-Only - default)
- name: "Phase 6: Summarize Changes"
id: phase6
if: steps.phase4.outputs.updates_required == 'true'
run: |
# Get git diff of docs
git diff docs/src/ > /tmp/docs-diff.txt || true
"$DROID_BIN" exec \
-m "$DROID_MODEL" \
-f .factory/prompts/docs-automation/phase6-summarize.md \
> /tmp/phase6-summary.md 2>&1 || true
echo "Summary generated"
cat /tmp/phase6-summary.md
# Phase 7: Commit and Open PR
- name: "Phase 7: Create PR"
id: phase7
if: steps.phase4.outputs.updates_required == 'true'
run: |
# Check if there are actual changes
if git diff --quiet docs/src/; then
echo "No documentation changes detected"
exit 0
fi
# Configure git
git config user.name "factory-droid[bot]"
git config user.email "138933559+factory-droid[bot]@users.noreply.github.com"
# Daily batch branch - one branch per day, multiple commits accumulate
BRANCH_NAME="docs/auto-update-$(date +%Y-%m-%d)"
# Stash local changes from phase 5
git stash push -m "docs-automation-changes" -- docs/src/
# Check if branch already exists on remote
if git ls-remote --exit-code --heads origin "$BRANCH_NAME" > /dev/null 2>&1; then
echo "Branch $BRANCH_NAME exists, checking out and updating..."
git fetch origin "$BRANCH_NAME"
git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"
else
echo "Creating new branch $BRANCH_NAME..."
git checkout -b "$BRANCH_NAME"
fi
# Apply stashed changes
git stash pop || true
# Stage and commit
git add docs/src/
SUMMARY=$(head -50 < /tmp/phase6-summary.md)
git commit -m "docs: auto-update documentation
${SUMMARY}
Triggered by: ${{ steps.changed.outputs.source }} ${{ steps.changed.outputs.ref }}
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>"
# Push
git push -u origin "$BRANCH_NAME"
# Check if PR already exists for this branch
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' || echo "")
if [ -n "$EXISTING_PR" ]; then
echo "PR #$EXISTING_PR already exists for branch $BRANCH_NAME, updated with new commit"
else
# Create new PR
gh pr create \
--title "docs: automated documentation update ($(date +%Y-%m-%d))" \
--body-file /tmp/phase6-summary.md \
--base main || true
echo "PR created on branch: $BRANCH_NAME"
fi
env:
GH_TOKEN: ${{ github.token }}
# Summary output
- name: "Summary"
if: always()
run: |
echo "## Documentation Automation Summary" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ steps.phase4.outputs.updates_required }}" == "false" ]; then
echo "No documentation updates required for this change." >> "$GITHUB_STEP_SUMMARY"
elif [ -f /tmp/phase6-summary.md ]; then
cat /tmp/phase6-summary.md >> "$GITHUB_STEP_SUMMARY"
else
echo "Workflow completed. Check individual phase outputs for details." >> "$GITHUB_STEP_SUMMARY"
fi

View File

@@ -66,7 +66,7 @@ jobs:
if: |-
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
(inputs.force-bump == 'true' || needs.check_bump_needed.outputs.needs_bump == 'true')
runs-on: namespace-profile-8x16-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
name: extension_bump::generate_token
@@ -79,7 +79,7 @@ jobs:
with:
clean: false
- name: extension_bump::install_bump_2_version
run: pip install bump2version
run: pip install bump2version --break-system-packages
shell: bash -euxo pipefail {0}
- id: bump-version
name: extension_bump::bump_version
@@ -119,7 +119,7 @@ jobs:
needs:
- check_bump_needed
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false'
runs-on: namespace-profile-8x16-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
name: extension_bump::generate_token

View File

@@ -13,7 +13,7 @@ on:
jobs:
create_release:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-8x16-ubuntu-2204
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: generate-token
name: extension_bump::generate_token

View File

@@ -51,7 +51,7 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_rust == 'true'
runs-on: namespace-profile-16x32-ubuntu-2204
runs-on: namespace-profile-4x8-ubuntu-2204
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -79,7 +79,7 @@ jobs:
needs:
- orchestrate
if: needs.orchestrate.outputs.check_extension == 'true'
runs-on: namespace-profile-2x4-ubuntu-2404
runs-on: namespace-profile-8x32-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

View File

@@ -0,0 +1,106 @@
# Generated from xtask::workflows::extension_workflow_rollout
# Rebuild with `cargo xtask workflows`.
name: extension_workflow_rollout
env:
CARGO_TERM_COLOR: always
on:
workflow_dispatch: {}
jobs:
fetch_extension_repos:
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: list-repos
name: extension_workflow_rollout::fetch_extension_repos::get_repositories
uses: actions/github-script@v7
with:
script: |
const repos = await github.paginate(github.rest.repos.listForOrg, {
org: 'zed-extensions',
type: 'public',
per_page: 100,
});
const filteredRepos = repos
.filter(repo => !repo.archived)
.filter(repo => repo.name !== 'workflows' && repo.name !== 'material-icon-theme')
.map(repo => repo.name);
console.log(`Found ${filteredRepos.length} extension repos`);
return filteredRepos;
result-encoding: json
outputs:
repos: ${{ steps.list-repos.outputs.result }}
timeout-minutes: 5
rollout_workflows_to_extension:
needs:
- fetch_extension_repos
if: needs.fetch_extension_repos.outputs.repos != '[]'
runs-on: namespace-profile-2x4-ubuntu-2404
strategy:
matrix:
repo: ${{ fromJson(needs.fetch_extension_repos.outputs.repos) }}
fail-fast: false
max-parallel: 5
steps:
- id: generate-token
name: extension_bump::generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
owner: zed-extensions
repositories: ${{ matrix.repo }}
permission-pull-requests: write
permission-contents: write
permission-workflows: write
- name: checkout_zed_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
path: zed
- name: steps::checkout_repo_with_token
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
token: ${{ steps.generate-token.outputs.token }}
repository: zed-extensions/${{ matrix.repo }}
path: extension
- name: extension_workflow_rollout::rollout_workflows_to_extension::copy_workflow_files
run: |
mkdir -p extension/.github/workflows
cp zed/extensions/workflows/shared/*.yml extension/.github/workflows/
shell: bash -euxo pipefail {0}
- id: short-sha
name: extension_workflow_rollout::rollout_workflows_to_extension::get_short_sha
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
working-directory: zed
- id: create-pr
name: extension_workflow_rollout::rollout_workflows_to_extension::create_pull_request
uses: peter-evans/create-pull-request@v7
with:
path: extension
title: Update CI workflows to `zed@${{ steps.short-sha.outputs.sha_short }}`
body: |
This PR updates the CI workflow files from the main Zed repository
based on the commit zed-industries/zed@${{ github.sha }}
commit-message: Update CI workflows to `zed@${{ steps.short-sha.outputs.sha_short }}`
branch: update-workflows
committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
author: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
base: main
delete-branch: true
token: ${{ steps.generate-token.outputs.token }}
sign-commits: true
- name: extension_workflow_rollout::rollout_workflows_to_extension::enable_auto_merge
run: |
PR_NUMBER="${{ steps.create-pr.outputs.pull-request-number }}"
if [ -n "$PR_NUMBER" ]; then
cd extension
gh pr merge "$PR_NUMBER" --auto --squash
fi
shell: bash -euxo pipefail {0}
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
timeout-minutes: 10

View File

@@ -6,7 +6,7 @@ on:
jobs:
handle-good-first-issue:
if: github.event.label.name == 'good first issue' && github.repository_owner == 'zed-industries'
if: github.event.label.name == '.contrib/good first issue' && github.repository_owner == 'zed-industries'
runs-on: ubuntu-latest
steps:

View File

@@ -74,12 +74,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large

View File

@@ -74,18 +74,12 @@ jobs:
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '9'
- name: ./script/prettier
- name: steps::prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=false
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -166,12 +160,6 @@ jobs:
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::trigger_autofix
if: failure() && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=true
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -365,6 +353,9 @@ jobs:
- name: steps::download_wasi_sdk
run: ./script/download-wasi-sdk
shell: bash -euxo pipefail {0}
- name: ./script/generate-action-metadata
run: ./script/generate-action-metadata
shell: bash -euxo pipefail {0}
- name: run_tests::check_docs::install_mdbook
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08
with:

1
.gitignore vendored
View File

@@ -36,6 +36,7 @@
DerivedData/
Packages
xcuserdata/
crates/docs_preprocessor/actions.json
# Don't commit any secrets to the repo.
.env

View File

@@ -23,7 +23,6 @@ In particular we love PRs that are:
If you're looking for concrete ideas:
- [Curated board of issues](https://github.com/orgs/zed-industries/projects/69) suitable for everyone from first-time contributors to seasoned community champions.
- [Triaged bugs with confirmed steps to reproduce](https://github.com/zed-industries/zed/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug%20label%3Astate%3Areproducible).
- [Area labels](https://github.com/zed-industries/zed/labels?q=area%3A*) to browse bugs in a specific part of the product you care about (after clicking on an area label, add type:Bug to the search).

97
Cargo.lock generated
View File

@@ -226,9 +226,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.9.0"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13"
checksum = "d3e527d7dfe0f334313d42d1d9318f0a79665f6f21c440d0798f230a77a7ed6c"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol-schema"
version = "0.10.0"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6"
checksum = "6903a00e8ac822f9bacac59a1932754d7387c72ebb7c9c7439ad021505591da4"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -268,6 +268,7 @@ dependencies = [
"client",
"collections",
"env_logger 0.11.8",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
@@ -793,7 +794,7 @@ dependencies = [
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.32.9",
"wayland-protocols",
"zbus",
]
@@ -2380,7 +2381,6 @@ dependencies = [
name = "buffer_diff"
version = "0.1.0"
dependencies = [
"anyhow",
"clock",
"ctor",
"futures 0.3.31",
@@ -3525,6 +3525,33 @@ dependencies = [
"theme",
]
[[package]]
name = "component_preview"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"component",
"db",
"fs",
"gpui",
"language",
"log",
"node_runtime",
"notifications",
"project",
"release_channel",
"reqwest_client",
"session",
"settings",
"theme",
"ui",
"ui_input",
"uuid",
"workspace",
]
[[package]]
name = "compression-codecs"
version = "0.4.31"
@@ -5021,8 +5048,6 @@ name = "docs_preprocessor"
version = "0.1.0"
dependencies = [
"anyhow",
"command_palette",
"gpui",
"mdbook",
"regex",
"serde",
@@ -5031,7 +5056,6 @@ dependencies = [
"task",
"theme",
"util",
"zed",
"zlog",
]
@@ -5188,6 +5212,7 @@ dependencies = [
"anyhow",
"arrayvec",
"brotli",
"buffer_diff",
"client",
"clock",
"cloud_api_types",
@@ -5225,7 +5250,9 @@ dependencies = [
"strum 0.27.2",
"telemetry",
"telemetry_events",
"text",
"thiserror 2.0.17",
"time",
"ui",
"util",
"uuid",
@@ -5330,8 +5357,10 @@ dependencies = [
"anyhow",
"buffer_diff",
"client",
"clock",
"cloud_llm_client",
"codestral",
"collections",
"command_palette_hooks",
"copilot",
"edit_prediction",
@@ -5340,18 +5369,20 @@ dependencies = [
"feature_flags",
"fs",
"futures 0.3.31",
"git",
"gpui",
"indoc",
"language",
"log",
"language_model",
"lsp",
"markdown",
"menu",
"multi_buffer",
"paths",
"pretty_assertions",
"project",
"regex",
"release_channel",
"semver",
"serde_json",
"settings",
"supermaven",
@@ -5364,6 +5395,7 @@ dependencies = [
"workspace",
"zed_actions",
"zeta_prompt",
"zlog",
]
[[package]]
@@ -7370,7 +7402,7 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols 0.31.2",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.3",
@@ -8932,6 +8964,8 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"extension",
"extension_host",
"fs",
"futures 0.3.31",
"google_ai",
@@ -12571,6 +12605,7 @@ dependencies = [
"gpui",
"language",
"menu",
"notifications",
"pretty_assertions",
"project",
"rayon",
@@ -12648,6 +12683,8 @@ dependencies = [
"paths",
"rope",
"serde",
"strum 0.27.2",
"tempfile",
"text",
"util",
"uuid",
@@ -18927,18 +18964,6 @@ dependencies = [
"xcursor",
]
[[package]]
name = "wayland-protocols"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.9"
@@ -18953,14 +18978,14 @@ dependencies = [
[[package]]
name = "wayland-protocols-plasma"
version = "0.2.0"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.31.2",
"wayland-protocols",
"wayland-scanner",
]
@@ -18973,7 +18998,7 @@ dependencies = [
"bitflags 2.9.4",
"wayland-backend",
"wayland-client",
"wayland-protocols 0.32.9",
"wayland-protocols",
"wayland-scanner",
]
@@ -20275,6 +20300,16 @@ dependencies = [
"zlog",
]
[[package]]
name = "worktree_benchmarks"
version = "0.1.0"
dependencies = [
"fs",
"gpui",
"settings",
"worktree",
]
[[package]]
name = "writeable"
version = "0.6.1"
@@ -20643,6 +20678,7 @@ dependencies = [
"collections",
"command_palette",
"component",
"component_preview",
"copilot",
"crashes",
"dap",
@@ -20748,7 +20784,6 @@ dependencies = [
"tree-sitter-md",
"tree-sitter-rust",
"ui",
"ui_input",
"ui_prompt",
"url",
"urlencoding",
@@ -20928,7 +20963,7 @@ dependencies = [
[[package]]
name = "zed_glsl"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -20942,7 +20977,7 @@ dependencies = [
[[package]]
name = "zed_proto"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"zed_extension_api 0.7.0",
]

View File

@@ -39,6 +39,7 @@ members = [
"crates/command_palette",
"crates/command_palette_hooks",
"crates/component",
"crates/component_preview",
"crates/context_server",
"crates/copilot",
"crates/crashes",
@@ -198,6 +199,7 @@ members = [
"crates/web_search_providers",
"crates/workspace",
"crates/worktree",
"crates/worktree_benchmarks",
"crates/x_ai",
"crates/zed",
"crates/zed_actions",
@@ -274,6 +276,7 @@ collections = { path = "crates/collections", version = "0.1.0" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
component = { path = "crates/component" }
component_preview = { path = "crates/component_preview" }
context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
@@ -438,7 +441,7 @@ ztracing_macro = { path = "crates/ztracing_macro" }
# External crates
#
agent-client-protocol = { version = "=0.9.0", features = ["unstable"] }
agent-client-protocol = { version = "=0.9.2", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"

View File

@@ -20,7 +20,6 @@ Other platforms are not yet available:
- [Building Zed for macOS](./docs/src/development/macos.md)
- [Building Zed for Linux](./docs/src/development/linux.md)
- [Building Zed for Windows](./docs/src/development/windows.md)
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
### Contributing

View File

@@ -55,6 +55,13 @@
"down": "menu::SelectNext",
},
},
{
"context": "menu",
"bindings": {
"right": "menu::SelectChild",
"left": "menu::SelectParent",
},
},
{
"context": "Editor",
"bindings": {
@@ -227,6 +234,7 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -240,6 +248,7 @@
"ctrl-alt-l": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
"alt-tab": "agent::CycleFavoriteModels",
"ctrl-shift-j": "agent::ToggleNavigationMenu",
"ctrl-alt-i": "agent::ToggleOptionsMenu",
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
@@ -252,7 +261,6 @@
"ctrl-y": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"ctrl-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -284,36 +292,6 @@
"ctrl-alt-t": "agent::NewThread",
},
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"bindings": {
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"ctrl-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "EditMessageEditor > Editor",
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"bindings": {
@@ -328,14 +306,25 @@
"ctrl-enter": "menu::Confirm",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
@@ -343,11 +332,7 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
"enter": "editor::Newline",
},
},
{
@@ -814,7 +799,7 @@
},
},
{
"context": "PromptEditor",
"context": "InlineAssistant",
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",
"ctrl-]": "agent::CycleNextInlineAssist",

View File

@@ -54,6 +54,13 @@
"ctrl-cmd-s": "workspace::ToggleWorktreeSecurity",
},
},
{
"context": "menu",
"bindings": {
"right": "menu::SelectChild",
"left": "menu::SelectParent",
},
},
{
"context": "Editor",
"use_key_equivalents": true,
@@ -267,6 +274,7 @@
"cmd-shift-g": "search::SelectPreviousMatch",
"cmd-k l": "agent::OpenRulesLibrary",
"alt-tab": "agent::CycleFavoriteModels",
"cmd-shift-v": "agent::PasteRaw",
},
},
{
@@ -281,6 +289,7 @@
"cmd-alt-p": "agent::ManageProfiles",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
"alt-tab": "agent::CycleFavoriteModels",
"cmd-shift-j": "agent::ToggleNavigationMenu",
"cmd-alt-m": "agent::ToggleOptionsMenu",
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
@@ -293,7 +302,6 @@
"cmd-y": "agent::AllowOnce",
"cmd-alt-y": "agent::AllowAlways",
"cmd-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -325,39 +333,6 @@
"cmd-alt-t": "agent::NewThread",
},
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"cmd-enter": "agent::ChatWithFollow",
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
},
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "agent::Chat",
"enter": "editor::Newline",
"cmd-i": "agent::ToggleProfileSelector",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
},
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
@@ -379,16 +354,25 @@
"cmd-enter": "menu::Confirm",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"cmd-enter": "agent::ChatWithFollow",
"cmd-shift-v": "agent::PasteRaw",
"cmd-i": "agent::ToggleProfileSelector",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -396,11 +380,7 @@
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
"enter": "editor::Newline",
},
},
{
@@ -880,7 +860,7 @@
},
},
{
"context": "PromptEditor",
"context": "InlineAssistant > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-alt-/": "agent::ToggleModelSelector",

View File

@@ -54,6 +54,13 @@
"down": "menu::SelectNext",
},
},
{
"context": "menu",
"bindings": {
"right": "menu::SelectChild",
"left": "menu::SelectParent",
},
},
{
"context": "Editor",
"use_key_equivalents": true,
@@ -227,6 +234,7 @@
"ctrl-g": "search::SelectNextMatch",
"ctrl-shift-g": "search::SelectPreviousMatch",
"ctrl-k l": "agent::OpenRulesLibrary",
"ctrl-shift-v": "agent::PasteRaw",
},
},
{
@@ -240,6 +248,7 @@
"shift-alt-l": "agent::OpenRulesLibrary",
"shift-alt-p": "agent::ManageProfiles",
"ctrl-i": "agent::ToggleProfileSelector",
"alt-tab": "agent::CycleFavoriteModels",
"shift-alt-/": "agent::ToggleModelSelector",
"shift-alt-j": "agent::ToggleNavigationMenu",
"shift-alt-i": "agent::ToggleOptionsMenu",
@@ -253,7 +262,6 @@
"shift-alt-a": "agent::AllowOnce",
"ctrl-alt-y": "agent::AllowAlways",
"shift-alt-z": "agent::RejectOnce",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -286,39 +294,6 @@
"ctrl-alt-t": "agent::NewThread",
},
},
{
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"enter": "editor::Newline",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
},
},
{
"context": "EditMessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"enter": "menu::Confirm",
"alt-enter": "editor::Newline",
},
},
{
"context": "AgentFeedbackMessageEditor > Editor",
"use_key_equivalents": true,
@@ -334,16 +309,25 @@
"ctrl-enter": "menu::Confirm",
},
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::ChatWithFollow",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"ctrl-shift-v": "agent::PasteRaw",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
"bindings": {
"enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
},
},
{
@@ -351,11 +335,7 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector",
"alt-tab": "agent::CycleFavoriteModels",
"enter": "editor::Newline",
},
},
{
@@ -823,7 +803,7 @@
},
},
{
"context": "PromptEditor",
"context": "InlineAssistant",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "agent::CyclePreviousInlineAssist",

View File

@@ -24,7 +24,7 @@
},
},
{
"context": "InlineAssistEditor",
"context": "InlineAssistant > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-backspace": "editor::Cancel",

View File

@@ -24,7 +24,7 @@
},
},
{
"context": "InlineAssistEditor",
"context": "InlineAssistant > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-backspace": "editor::Cancel",

View File

@@ -1178,6 +1178,10 @@
"remove_trailing_whitespace_on_save": true,
// Whether to start a new line with a comment when a previous line is a comment as well.
"extend_comment_on_newline": true,
// Whether to continue markdown lists when pressing enter.
"extend_list_on_newline": true,
// Whether to indent list items when pressing tab after a list marker.
"indent_list_on_tab": true,
// Removes any lines containing only whitespace at the end of the file and
// ensures just one newline at the end.
"ensure_final_newline_on_save": true,
@@ -1321,6 +1325,14 @@
"hidden_files": ["**/.*"],
// Git gutter behavior configuration.
"git": {
// Global switch to enable or disable all git integration features.
// If set to true, disables all git integration features.
// If set to false, individual git integration features below will be independently enabled or disabled.
"disable_git": false,
// Whether to enable git status tracking.
"enable_status": true,
// Whether to enable git diff display.
"enable_diff": true,
// Control whether the git gutter is shown. May take 2 values:
// 1. Show the gutter
// "git_gutter": "tracked_files"

View File

@@ -11,6 +11,7 @@ use language::language_settings::FormatOnSave;
pub use mention::*;
use project::lsp_store::{FormatTrigger, LspFormatTarget};
use serde::{Deserialize, Serialize};
use serde_json::to_string_pretty;
use settings::Settings as _;
use task::{Shell, ShellBuilder};
pub use terminal::*;
@@ -192,6 +193,7 @@ pub struct ToolCall {
pub locations: Vec<acp::ToolCallLocation>,
pub resolved_locations: Vec<Option<AgentLocation>>,
pub raw_input: Option<serde_json::Value>,
pub raw_input_markdown: Option<Entity<Markdown>>,
pub raw_output: Option<serde_json::Value>,
}
@@ -222,6 +224,11 @@ impl ToolCall {
}
}
let raw_input_markdown = tool_call
.raw_input
.as_ref()
.and_then(|input| markdown_for_raw_output(input, &language_registry, cx));
let result = Self {
id: tool_call.tool_call_id,
label: cx
@@ -232,6 +239,7 @@ impl ToolCall {
resolved_locations: Vec::default(),
status,
raw_input: tool_call.raw_input,
raw_input_markdown,
raw_output: tool_call.raw_output,
};
Ok(result)
@@ -307,6 +315,7 @@ impl ToolCall {
}
if let Some(raw_input) = raw_input {
self.raw_input_markdown = markdown_for_raw_output(&raw_input, &language_registry, cx);
self.raw_input = Some(raw_input);
}
@@ -875,6 +884,7 @@ pub enum AcpThreadEvent {
Refusal,
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
ModeUpdated(acp::SessionModeId),
ConfigOptionsUpdated(Vec<acp::SessionConfigOption>),
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
@@ -1184,6 +1194,10 @@ impl AcpThread {
current_mode_id,
..
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
config_options,
..
}) => cx.emit(AcpThreadEvent::ConfigOptionsUpdated(config_options)),
_ => {}
}
Ok(())
@@ -1355,6 +1369,7 @@ impl AcpThread {
locations: Vec::new(),
resolved_locations: Vec::new(),
raw_input: None,
raw_input_markdown: None,
raw_output: None,
};
self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx);
@@ -1983,37 +1998,42 @@ impl AcpThread {
fn update_last_checkpoint(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let git_store = self.project.read(cx).git_store().clone();
let old_checkpoint = if let Some((_, message)) = self.last_user_message() {
if let Some(checkpoint) = message.checkpoint.as_ref() {
checkpoint.git_checkpoint.clone()
} else {
return Task::ready(Ok(()));
}
} else {
let Some((_, message)) = self.last_user_message() else {
return Task::ready(Ok(()));
};
let Some(user_message_id) = message.id.clone() else {
return Task::ready(Ok(()));
};
let Some(checkpoint) = message.checkpoint.as_ref() else {
return Task::ready(Ok(()));
};
let old_checkpoint = checkpoint.git_checkpoint.clone();
let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx));
cx.spawn(async move |this, cx| {
let new_checkpoint = new_checkpoint
let Some(new_checkpoint) = new_checkpoint
.await
.context("failed to get new checkpoint")
.log_err();
if let Some(new_checkpoint) = new_checkpoint {
let equal = git_store
.update(cx, |git, cx| {
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
})?
.await
.unwrap_or(true);
this.update(cx, |this, cx| {
let (ix, message) = this.last_user_message().context("no user message")?;
let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?;
checkpoint.show = !equal;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
anyhow::Ok(())
})??;
}
.log_err()
else {
return Ok(());
};
let equal = git_store
.update(cx, |git, cx| {
git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx)
})?
.await
.unwrap_or(true);
this.update(cx, |this, cx| {
if let Some((ix, message)) = this.user_message_mut(&user_message_id) {
if let Some(checkpoint) = message.checkpoint.as_mut() {
checkpoint.show = !equal;
cx.emit(AcpThreadEvent::EntryUpdated(ix));
}
}
})?;
Ok(())
})
@@ -2413,8 +2433,10 @@ fn markdown_for_raw_output(
)
})),
value => Some(cx.new(|cx| {
let pretty_json = to_string_pretty(value).unwrap_or_else(|_| value.to_string());
Markdown::new(
format!("```json\n{}\n```", value).into(),
format!("```json\n{}\n```", pretty_json).into(),
Some(language_registry.clone()),
None,
cx,
@@ -4057,4 +4079,67 @@ mod tests {
"Should have exactly 2 terminals (the completed ones from before checkpoint)"
);
}
/// Tests that update_last_checkpoint correctly updates the original message's checkpoint
/// even when a new user message is added while the async checkpoint comparison is in progress.
///
/// This is a regression test for a bug where update_last_checkpoint would fail with
/// "no checkpoint" if a new user message (without a checkpoint) was added between when
/// update_last_checkpoint started and when its async closure ran.
#[gpui::test]
async fn test_update_last_checkpoint_with_new_message_added(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/test"), json!({".git": {}, "file.txt": "content"}))
.await;
let project = Project::test(fs.clone(), [Path::new(path!("/test"))], cx).await;
let handler_done = Arc::new(AtomicBool::new(false));
let handler_done_clone = handler_done.clone();
let connection = Rc::new(FakeAgentConnection::new().on_user_message(
move |_, _thread, _cx| {
handler_done_clone.store(true, SeqCst);
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }.boxed_local()
},
));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.await
.unwrap();
let send_future = thread.update(cx, |thread, cx| thread.send_raw("First message", cx));
let send_task = cx.background_executor.spawn(send_future);
// Tick until handler completes, then a few more to let update_last_checkpoint start
while !handler_done.load(SeqCst) {
cx.executor().tick();
}
for _ in 0..5 {
cx.executor().tick();
}
thread.update(cx, |thread, cx| {
thread.push_entry(
AgentThreadEntry::UserMessage(UserMessage {
id: Some(UserMessageId::new()),
content: ContentBlock::Empty,
chunks: vec!["Injected message (no checkpoint)".into()],
checkpoint: None,
indented: false,
}),
cx,
);
});
cx.run_until_parked();
let result = send_task.await;
assert!(
result.is_ok(),
"send should succeed even when new message added during update_last_checkpoint: {:?}",
result.err()
);
}
}

View File

@@ -86,6 +86,14 @@ pub trait AgentConnection {
None
}
fn session_config_options(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionConfigOptions>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -125,6 +133,26 @@ pub trait AgentSessionModes {
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
}
pub trait AgentSessionConfigOptions {
/// Get all current config options with their state
fn config_options(&self) -> Vec<acp::SessionConfigOption>;
/// Set a config option value
/// Returns the full updated list of config options
fn set_config_option(
&self,
config_id: acp::SessionConfigId,
value: acp::SessionConfigValueId,
cx: &mut App,
) -> Task<Result<Vec<acp::SessionConfigOption>>>;
/// Whenever the config options are updated the receiver will be notified.
/// Optional for agents that don't update their config options dynamically.
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
None
}
}
#[derive(Debug)]
pub struct AuthRequired {
pub description: Option<String>,
@@ -202,12 +230,15 @@ pub trait AgentModelSelector: 'static {
fn should_render_footer(&self) -> bool {
false
}
}
/// Whether this selector supports the favorites feature.
/// Only the native agent uses the model ID format that maps to settings.
fn supports_favorites(&self) -> bool {
false
}
/// Icon for a model in the model selector.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AgentModelIcon {
/// A built-in icon from Zed's icon set.
Named(IconName),
/// Path to a custom SVG icon file.
Path(SharedString),
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -215,7 +246,7 @@ pub struct AgentModelInfo {
pub id: acp::ModelId,
pub name: SharedString,
pub description: Option<SharedString>,
pub icon: Option<IconName>,
pub icon: Option<AgentModelIcon>,
}
impl From<acp::ModelInfo> for AgentModelInfo {

View File

@@ -1,10 +1,10 @@
use anyhow::Result;
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use buffer_diff::BufferDiff;
use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
use itertools::Itertools;
use language::{
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer,
Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, TextBuffer,
};
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
use util::ResultExt;
@@ -49,15 +49,15 @@ impl Diff {
.update(cx, |multibuffer, cx| {
let hunk_ranges = {
let buffer = buffer.read(cx);
let diff = diff.read(cx);
diff.hunks_intersecting_range(
Anchor::min_for_buffer(buffer.remote_id())
..Anchor::max_for_buffer(buffer.remote_id()),
buffer,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>()
diff.read(cx)
.snapshot(cx)
.hunks_intersecting_range(
Anchor::min_for_buffer(buffer.remote_id())
..Anchor::max_for_buffer(buffer.remote_id()),
buffer,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>()
};
multibuffer.set_excerpts_for_path(
@@ -86,17 +86,9 @@ impl Diff {
pub fn new(buffer: Entity<Buffer>, cx: &mut Context<Self>) -> Self {
let buffer_text_snapshot = buffer.read(cx).text_snapshot();
let base_text_snapshot = buffer.read(cx).snapshot();
let base_text = base_text_snapshot.text();
debug_assert_eq!(buffer_text_snapshot.text(), base_text);
let buffer_diff = cx.new(|cx| {
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot);
let snapshot = diff.snapshot(cx);
let secondary_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer_text_snapshot, cx);
diff.set_snapshot(snapshot, &buffer_text_snapshot, cx);
diff
});
let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, cx);
let secondary_diff = cx.new(|cx| BufferDiff::new_unchanged(&buffer_text_snapshot, cx));
diff.set_secondary_diff(secondary_diff);
diff
});
@@ -109,7 +101,7 @@ impl Diff {
Self::Pending(PendingDiff {
multibuffer,
base_text: Arc::new(base_text),
base_text: Arc::from(buffer_text_snapshot.text().as_str()),
_subscription: cx.observe(&buffer, |this, _, cx| {
if let Diff::Pending(diff) = this {
diff.update(cx);
@@ -176,7 +168,7 @@ impl Diff {
new_buffer,
..
}) => {
base_text.as_str() != old_text
base_text.as_ref() != old_text
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
}
Diff::Finalized(FinalizedDiff {
@@ -184,7 +176,7 @@ impl Diff {
new_buffer,
..
}) => {
base_text.as_str() != old_text
base_text.as_ref() != old_text
|| !new_buffer.read(cx).as_rope().chunks().equals_str(new_text)
}
}
@@ -193,7 +185,7 @@ impl Diff {
pub struct PendingDiff {
multibuffer: Entity<MultiBuffer>,
base_text: Arc<String>,
base_text: Arc<str>,
new_buffer: Entity<Buffer>,
diff: Entity<BufferDiff>,
revealed_ranges: Vec<Range<Anchor>>,
@@ -208,21 +200,22 @@ impl PendingDiff {
let base_text = self.base_text.clone();
self.update_diff = cx.spawn(async move |diff, cx| {
let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?;
let diff_snapshot = BufferDiff::update_diff(
buffer_diff.clone(),
text_snapshot.clone(),
Some(base_text),
false,
false,
None,
None,
cx,
)
.await?;
let language = buffer.read_with(cx, |buffer, _| buffer.language().cloned())?;
let update = buffer_diff
.update(cx, |diff, cx| {
diff.update_diff(
text_snapshot.clone(),
Some(base_text.clone()),
false,
language,
cx,
)
})?
.await;
buffer_diff.update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
diff.set_snapshot(update.clone(), &text_snapshot, cx);
diff.secondary_diff().unwrap().update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx);
diff.set_snapshot(update, &text_snapshot, cx);
});
})?;
diff.update(cx, |diff, cx| {
@@ -319,13 +312,14 @@ impl PendingDiff {
fn excerpt_ranges(&self, cx: &App) -> Vec<Range<Point>> {
let buffer = self.new_buffer.read(cx);
let diff = self.diff.read(cx);
let mut ranges = diff
let mut ranges = self
.diff
.read(cx)
.snapshot(cx)
.hunks_intersecting_range(
Anchor::min_for_buffer(buffer.remote_id())
..Anchor::max_for_buffer(buffer.remote_id()),
buffer,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer))
.collect::<Vec<_>>();
@@ -357,60 +351,47 @@ impl PendingDiff {
pub struct FinalizedDiff {
path: String,
base_text: Arc<String>,
base_text: Arc<str>,
new_buffer: Entity<Buffer>,
multibuffer: Entity<MultiBuffer>,
_update_diff: Task<Result<()>>,
}
async fn build_buffer_diff(
old_text: Arc<String>,
old_text: Arc<str>,
buffer: &Entity<Buffer>,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut AsyncApp,
) -> Result<Entity<BufferDiff>> {
let language = cx.update(|cx| buffer.read(cx).language().cloned())?;
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
let old_text_rope = cx
.background_spawn({
let old_text = old_text.clone();
async move { Rope::from(old_text.as_str()) }
})
.await;
let base_buffer = cx
.update(|cx| {
Buffer::build_snapshot(
old_text_rope,
buffer.language().cloned(),
language_registry,
cx,
)
})?
.await;
let secondary_diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
let diff_snapshot = cx
.update(|cx| {
BufferDiffSnapshot::new_with_base_buffer(
let update = secondary_diff
.update(cx, |secondary_diff, cx| {
secondary_diff.update_diff(
buffer.text.clone(),
Some(old_text),
base_buffer,
true,
language.clone(),
cx,
)
})?
.await;
let secondary_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer, cx);
diff.set_snapshot(diff_snapshot.clone(), &buffer, cx);
diff
secondary_diff.update(cx, |secondary_diff, cx| {
secondary_diff.language_changed(language.clone(), language_registry.clone(), cx);
secondary_diff.set_snapshot(update.clone(), &buffer, cx);
})?;
cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer.text, cx);
diff.set_snapshot(diff_snapshot, &buffer, cx);
let diff = cx.new(|cx| BufferDiff::new(&buffer, cx))?;
diff.update(cx, |diff, cx| {
diff.language_changed(language, language_registry, cx);
diff.set_snapshot(update.clone(), &buffer, cx);
diff.set_secondary_diff(secondary_diff);
diff
})
})?;
Ok(diff)
}
#[cfg(test)]

View File

@@ -4,22 +4,20 @@ use std::{
fmt::Display,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
};
use agent_client_protocol as acp;
use collections::HashMap;
use gpui::{
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
prelude::*,
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
};
use language::LanguageRegistry;
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
use project::Project;
use settings::Settings;
use theme::ThemeSettings;
use ui::{Tooltip, WithScrollbar, prelude::*};
use ui::{CopyButton, Tooltip, WithScrollbar, prelude::*};
use util::ResultExt as _;
use workspace::{
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -544,15 +542,11 @@ impl Render for AcpTools {
pub struct AcpToolsToolbarItemView {
acp_tools: Option<Entity<AcpTools>>,
just_copied: bool,
}
impl AcpToolsToolbarItemView {
pub fn new() -> Self {
Self {
acp_tools: None,
just_copied: false,
}
Self { acp_tools: None }
}
}
@@ -572,37 +566,14 @@ impl Render for AcpToolsToolbarItemView {
h_flex()
.gap_2()
.child({
let acp_tools = acp_tools.clone();
IconButton::new(
"copy_all_messages",
if self.just_copied {
IconName::Check
} else {
IconName::Copy
},
)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(if self.just_copied {
"Copied!"
} else {
"Copy All Messages"
}))
.disabled(!has_messages)
.on_click(cx.listener(move |this, _, _window, cx| {
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
cx.write_to_clipboard(ClipboardItem::new_string(content));
let message = acp_tools
.read(cx)
.serialize_observed_messages()
.unwrap_or_default();
this.just_copied = true;
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
this.update(cx, |this, cx| {
this.just_copied = false;
cx.notify();
})
})
.detach();
}
}))
CopyButton::new(message)
.tooltip_label("Copy All Messages")
.disabled(!has_messages)
})
.child(
IconButton::new("clear_messages", IconName::Trash)

View File

@@ -6,7 +6,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
use language::{Anchor, Buffer, BufferEvent, Point, ToPoint};
use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
use std::{cmp, ops::Range, sync::Arc};
use text::{Edit, Patch, Rope};
@@ -150,7 +150,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
.is_some_and(|file| file.disk_state().is_deleted())
{
// If the buffer had been edited by a tool, but it got
// deleted externally, we want to stop tracking it.
@@ -162,7 +162,7 @@ impl ActionLog {
if buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
.is_some_and(|file| !file.disk_state().is_deleted())
{
// If the buffer had been deleted by a tool, but it got
// resurrected externally, we want to clear the edits we
@@ -262,7 +262,7 @@ impl ActionLog {
);
}
(Arc::new(base_text.to_string()), base_text)
(Arc::from(base_text.to_string().as_str()), base_text)
}
});
@@ -302,7 +302,7 @@ impl ActionLog {
.context("buffer not tracked")?;
let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone();
let agent_diff_base = tracked_buffer.diff_base.clone();
let git_diff_base = git_diff.read(cx).base_text().as_rope().clone();
let git_diff_base = git_diff.read(cx).base_text(cx).as_rope().clone();
let buffer_text = tracked_buffer.snapshot.as_rope().clone();
anyhow::Ok(cx.background_spawn(async move {
let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable();
@@ -352,7 +352,7 @@ impl ActionLog {
}
(
Arc::new(new_agent_diff_base.to_string()),
Arc::from(new_agent_diff_base.to_string().as_str()),
new_agent_diff_base,
)
}))
@@ -374,11 +374,11 @@ impl ActionLog {
this: &WeakEntity<ActionLog>,
buffer: &Entity<Buffer>,
buffer_snapshot: text::BufferSnapshot,
new_base_text: Arc<String>,
new_base_text: Arc<str>,
new_diff_base: Rope,
cx: &mut AsyncApp,
) -> Result<()> {
let (diff, language, language_registry) = this.read_with(cx, |this, cx| {
let (diff, language) = this.read_with(cx, |this, cx| {
let tracked_buffer = this
.tracked_buffers
.get(buffer)
@@ -386,25 +386,28 @@ impl ActionLog {
anyhow::Ok((
tracked_buffer.diff.clone(),
buffer.read(cx).language().cloned(),
buffer.read(cx).language_registry(),
))
})??;
let diff_snapshot = BufferDiff::update_diff(
diff.clone(),
buffer_snapshot.clone(),
Some(new_base_text),
true,
false,
language,
language_registry,
cx,
)
.await;
let update = diff.update(cx, |diff, cx| {
diff.update_diff(
buffer_snapshot.clone(),
Some(new_base_text),
true,
language,
cx,
)
});
let mut unreviewed_edits = Patch::default();
if let Ok(diff_snapshot) = diff_snapshot {
if let Ok(update) = update {
let update = update.await;
let diff_snapshot = diff.update(cx, |diff, cx| {
diff.set_snapshot(update.clone(), &buffer_snapshot, cx);
diff.snapshot(cx)
})?;
unreviewed_edits = cx
.background_spawn({
let diff_snapshot = diff_snapshot.clone();
let buffer_snapshot = buffer_snapshot.clone();
let new_diff_base = new_diff_base.clone();
async move {
@@ -431,10 +434,6 @@ impl ActionLog {
}
})
.await;
diff.update(cx, |diff, cx| {
diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx);
})?;
}
this.update(cx, |this, cx| {
let tracked_buffer = this
@@ -769,7 +768,7 @@ impl ActionLog {
tracked.version != buffer.version
&& buffer
.file()
.is_some_and(|file| file.disk_state() != DiskState::Deleted)
.is_some_and(|file| !file.disk_state().is_deleted())
})
.map(|(buffer, _)| buffer)
}
@@ -975,7 +974,8 @@ impl TrackedBuffer {
fn has_edits(&self, cx: &App) -> bool {
self.diff
.read(cx)
.hunks(self.buffer.read(cx), cx)
.snapshot(cx)
.hunks(self.buffer.read(cx))
.next()
.is_some()
}
@@ -2388,13 +2388,14 @@ mod tests {
(
buffer,
diff.read(cx)
.hunks(&snapshot, cx)
.snapshot(cx)
.hunks(&snapshot)
.map(|hunk| HunkStatus {
diff_status: hunk.status().kind,
range: hunk.range,
old_text: diff
.read(cx)
.base_text()
.base_text(cx)
.text_for_range(hunk.diff_base_byte_range)
.collect(),
})

View File

@@ -30,7 +30,7 @@ use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use language_model::{IconOrSvg, LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
@@ -93,7 +93,7 @@ impl LanguageModels {
fn refresh_list(&mut self, cx: &App) {
let providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -153,7 +153,10 @@ impl LanguageModels {
id: Self::model_id(model),
name: model.name().0,
description: None,
icon: Some(provider.icon()),
icon: Some(match provider.icon() {
IconOrSvg::Svg(path) => acp_thread::AgentModelIcon::Path(path),
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
}),
}
}
@@ -164,7 +167,7 @@ impl LanguageModels {
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -426,7 +429,7 @@ impl NativeAgent {
.into_iter()
.flat_map(|(contents, prompt_metadata)| match contents {
Ok(contents) => Some(UserRulesContext {
uuid: prompt_metadata.id.user_id()?,
uuid: prompt_metadata.id.as_user()?,
title: prompt_metadata.title.map(|title| title.to_string()),
contents,
}),
@@ -1164,10 +1167,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
fn should_render_footer(&self) -> bool {
true
}
fn supports_favorites(&self) -> bool {
true
}
}
impl acp_thread::AgentConnection for NativeAgentConnection {
@@ -1630,7 +1629,9 @@ mod internal_tests {
id: acp::ModelId::new("fake/fake"),
name: "Fake".into(),
description: None,
icon: Some(ui::IconName::ZedAssistant),
icon: Some(acp_thread::AgentModelIcon::Named(
ui::IconName::ZedAssistant
)),
}]
)])
);

View File

@@ -1,10 +1,14 @@
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
use agent_client_protocol as acp;
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_settings::AgentSettings;
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
use gpui::{App, Entity, SharedString, Task};
use prompt_store::PromptStore;
use settings::{LanguageModelSelection, Settings as _, update_settings_file};
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
@@ -71,6 +75,38 @@ impl AgentServer for NativeAgentServer {
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
AgentSettings::get_global(cx).favorite_model_ids()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let selection = model_id_to_selection(&model_id);
update_settings_file(fs, cx, move |settings, _| {
let agent = settings.agent.get_or_insert_default();
if should_be_favorite {
agent.add_favorite_model(selection.clone());
} else {
agent.remove_favorite_model(&selection);
}
});
}
}
/// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
let id = model_id.0.as_ref();
let (provider, model) = id.split_once('/').unwrap_or(("", id));
LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
}
}
#[cfg(test)]

View File

@@ -5,7 +5,7 @@ use futures::StreamExt;
use gpui::{App, Entity, SharedString, Task};
use language::{OffsetRangeExt, ParseStatus, Point};
use project::{
Project, WorktreeSettings,
Project, SearchResults, WorktreeSettings,
search::{SearchQuery, SearchResult},
};
use schemars::JsonSchema;
@@ -176,14 +176,17 @@ impl AgentTool for GrepTool {
let project = self.project.downgrade();
cx.spawn(async move |cx| {
futures::pin_mut!(results);
// Keep the search alive for the duration of result iteration. Dropping this task is the
// cancellation mechanism; we intentionally do not detach it.
let SearchResults {rx, _task_handle} = results;
futures::pin_mut!(rx);
let mut output = String::new();
let mut skips_remaining = input.offset;
let mut matches_found = 0;
let mut has_more_matches = false;
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await {
'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = rx.next().await {
if ranges.is_empty() {
continue;
}

View File

@@ -21,6 +21,7 @@ acp_tools.workspace = true
acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true
feature_flags.workspace = true
anyhow.workspace = true
async-trait.workspace = true
client.workspace = true

View File

@@ -4,6 +4,7 @@ use action_log::ActionLog;
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
use anyhow::anyhow;
use collections::HashMap;
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
use futures::AsyncBufReadExt as _;
use futures::io::BufReader;
use project::Project;
@@ -12,8 +13,10 @@ use serde::Deserialize;
use settings::Settings as _;
use task::ShellBuilder;
use util::ResultExt as _;
use util::process::Child;
use std::path::PathBuf;
use std::process::Stdio;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error;
@@ -38,20 +41,37 @@ pub struct AcpConnection {
agent_capabilities: acp::AgentCapabilities,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
default_config_options: HashMap<String, String>,
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,
child: Child,
_io_task: Task<Result<(), acp::Error>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
}
struct ConfigOptions {
config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
tx: Rc<RefCell<watch::Sender<()>>>,
rx: watch::Receiver<()>,
}
impl ConfigOptions {
fn new(config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>) -> Self {
let (tx, rx) = watch::channel(());
Self {
config_options,
tx: Rc::new(RefCell::new(tx)),
rx,
}
}
}
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
models: Option<Rc<RefCell<acp::SessionModelState>>>,
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
config_options: Option<ConfigOptions>,
}
pub async fn connect(
@@ -60,6 +80,7 @@ pub async fn connect(
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
default_config_options: HashMap<String, String>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
@@ -69,6 +90,7 @@ pub async fn connect(
root_dir,
default_mode,
default_model,
default_config_options,
is_remote,
cx,
)
@@ -85,22 +107,19 @@ impl AcpConnection {
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
default_model: Option<acp::ModelId>,
default_config_options: HashMap<String, String>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
let mut child =
builder.build_command(Some(command.path.display().to_string()), &command.args);
child
.envs(command.env.iter().flatten())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
builder.build_std_command(Some(command.path.display().to_string()), &command.args);
child.envs(command.env.iter().flatten());
if !is_remote {
child.current_dir(root_dir);
}
let mut child = child.spawn()?;
let mut child = Child::spawn(child, Stdio::piped(), Stdio::piped(), Stdio::piped())?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
@@ -217,6 +236,7 @@ impl AcpConnection {
agent_capabilities: response.agent_capabilities,
default_mode,
default_model,
default_config_options,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
@@ -235,7 +255,6 @@ impl AcpConnection {
impl Drop for AcpConnection {
fn drop(&mut self) {
// See the comment on the child field.
self.child.kill().log_err();
}
}
@@ -256,6 +275,7 @@ impl AgentConnection for AcpConnection {
let sessions = self.sessions.clone();
let default_mode = self.default_mode.clone();
let default_model = self.default_model.clone();
let default_config_options = self.default_config_options.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = if project.read(cx).is_local() {
@@ -322,8 +342,21 @@ impl AgentConnection for AcpConnection {
}
})?;
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
let use_config_options = cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>())?;
// Config options take precedence over legacy modes/models
let (modes, models, config_options) = if use_config_options && let Some(opts) = response.config_options {
(
None,
None,
Some(Rc::new(RefCell::new(opts))),
)
} else {
// Fall back to legacy modes/models
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
(modes, models, None)
};
if let Some(default_mode) = default_mode {
if let Some(modes) = modes.as_ref() {
@@ -411,6 +444,92 @@ impl AgentConnection for AcpConnection {
}
}
if let Some(config_opts) = config_options.as_ref() {
let defaults_to_apply: Vec<_> = {
let config_opts_ref = config_opts.borrow();
config_opts_ref
.iter()
.filter_map(|config_option| {
let default_value = default_config_options.get(&*config_option.id.0)?;
let is_valid = match &config_option.kind {
acp::SessionConfigKind::Select(select) => match &select.options {
acp::SessionConfigSelectOptions::Ungrouped(options) => {
options.iter().any(|opt| &*opt.value.0 == default_value.as_str())
}
acp::SessionConfigSelectOptions::Grouped(groups) => groups
.iter()
.any(|g| g.options.iter().any(|opt| &*opt.value.0 == default_value.as_str())),
_ => false,
},
_ => false,
};
if is_valid {
let initial_value = match &config_option.kind {
acp::SessionConfigKind::Select(select) => {
Some(select.current_value.clone())
}
_ => None,
};
Some((config_option.id.clone(), default_value.clone(), initial_value))
} else {
log::warn!(
"`{}` is not a valid value for config option `{}` in {}",
default_value,
config_option.id.0,
name
);
None
}
})
.collect()
};
for (config_id, default_value, initial_value) in defaults_to_apply {
cx.spawn({
let default_value_id = acp::SessionConfigValueId::new(default_value.clone());
let session_id = response.session_id.clone();
let config_id_clone = config_id.clone();
let config_opts = config_opts.clone();
let conn = conn.clone();
async move |_| {
let result = conn
.set_session_config_option(
acp::SetSessionConfigOptionRequest::new(
session_id,
config_id_clone.clone(),
default_value_id,
),
)
.await
.log_err();
if result.is_none() {
if let Some(initial) = initial_value {
let mut opts = config_opts.borrow_mut();
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id_clone) {
if let acp::SessionConfigKind::Select(select) =
&mut opt.kind
{
select.current_value = initial;
}
}
}
}
}
})
.detach();
let mut opts = config_opts.borrow_mut();
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id) {
if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
select.current_value = acp::SessionConfigValueId::new(default_value);
}
}
}
}
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {
@@ -432,6 +551,7 @@ impl AgentConnection for AcpConnection {
suppress_abort_err: false,
session_modes: modes,
models,
config_options: config_options.map(|opts| ConfigOptions::new(opts))
};
sessions.borrow_mut().insert(session_id, session);
@@ -567,6 +687,25 @@ impl AgentConnection for AcpConnection {
}
}
fn session_config_options(
&self,
session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionConfigOptions>> {
let sessions = self.sessions.borrow();
let session = sessions.get(session_id)?;
let config_opts = session.config_options.as_ref()?;
Some(Rc::new(AcpSessionConfigOptions {
session_id: session_id.clone(),
connection: self.connection.clone(),
state: config_opts.config_options.clone(),
watch_tx: config_opts.tx.clone(),
watch_rx: config_opts.rx.clone(),
}) as _)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
@@ -685,6 +824,49 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
}
}
struct AcpSessionConfigOptions {
session_id: acp::SessionId,
connection: Rc<acp::ClientSideConnection>,
state: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
watch_tx: Rc<RefCell<watch::Sender<()>>>,
watch_rx: watch::Receiver<()>,
}
impl acp_thread::AgentSessionConfigOptions for AcpSessionConfigOptions {
fn config_options(&self) -> Vec<acp::SessionConfigOption> {
self.state.borrow().clone()
}
fn set_config_option(
&self,
config_id: acp::SessionConfigId,
value: acp::SessionConfigValueId,
cx: &mut App,
) -> Task<Result<Vec<acp::SessionConfigOption>>> {
let connection = self.connection.clone();
let session_id = self.session_id.clone();
let state = self.state.clone();
let watch_tx = self.watch_tx.clone();
cx.foreground_executor().spawn(async move {
let response = connection
.set_session_config_option(acp::SetSessionConfigOptionRequest::new(
session_id, config_id, value,
))
.await?;
*state.borrow_mut() = response.config_options.clone();
watch_tx.borrow_mut().send(()).ok();
Ok(response.config_options)
})
}
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
Some(self.watch_rx.clone())
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
@@ -778,6 +960,21 @@ impl acp::Client for ClientDelegate {
}
}
if let acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
config_options,
..
}) = &notification.update
{
if let Some(opts) = &session.config_options {
*opts.config_options.borrow_mut() = config_options.clone();
opts.tx.borrow_mut().send(()).ok();
} else {
log::error!(
"Got a `ConfigOptionUpdate` notification, but the agent didn't specify `config_options` during session setup."
);
}
}
// Clone so we can inspect meta both before and after handing off to the thread
let update_clone = notification.update.clone();

View File

@@ -10,7 +10,7 @@ pub mod e2e_tests;
pub use claude::*;
use client::ProxySettings;
pub use codex::*;
use collections::HashMap;
use collections::{HashMap, HashSet};
pub use custom::*;
use fs::Fs;
pub use gemini::*;
@@ -56,9 +56,19 @@ impl AgentServerDelegate {
pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
fn connect(
&self,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
fn default_mode(&self, _cx: &App) -> Option<agent_client_protocol::SessionModeId> {
None
}
fn set_default_mode(
&self,
_mode_id: Option<agent_client_protocol::SessionModeId>,
@@ -67,7 +77,7 @@ pub trait AgentServer: Send {
) {
}
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
fn default_model(&self, _cx: &App) -> Option<agent_client_protocol::ModelId> {
None
}
@@ -79,14 +89,49 @@ pub trait AgentServer: Send {
) {
}
fn connect(
&self,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
HashSet::default()
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
fn default_config_option(&self, _config_id: &str, _cx: &App) -> Option<String> {
None
}
fn set_default_config_option(
&self,
_config_id: &str,
_value_id: Option<&str>,
_fs: Arc<dyn Fs>,
_cx: &mut App,
) {
}
fn favorite_config_option_value_ids(
&self,
_config_id: &agent_client_protocol::SessionConfigId,
_cx: &mut App,
) -> HashSet<agent_client_protocol::SessionConfigValueId> {
HashSet::default()
}
fn toggle_favorite_config_option_value(
&self,
_config_id: agent_client_protocol::SessionConfigId,
_value_id: agent_client_protocol::SessionConfigValueId,
_should_be_favorite: bool,
_fs: Arc<dyn Fs>,
_cx: &App,
) {
}
fn toggle_favorite_model(
&self,
_model_id: agent_client_protocol::ModelId,
_should_be_favorite: bool,
_fs: Arc<dyn Fs>,
_cx: &App,
) {
}
}
impl dyn AgentServer {

View File

@@ -1,4 +1,5 @@
use agent_client_protocol as acp;
use collections::HashSet;
use fs::Fs;
use settings::{SettingsStore, update_settings_file};
use std::path::Path;
@@ -30,7 +31,7 @@ impl AgentServer for ClaudeCode {
ui::IconName::AiClaude
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
@@ -51,7 +52,7 @@ impl AgentServer for ClaudeCode {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
@@ -72,6 +73,139 @@ impl AgentServer for ClaudeCode {
});
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.map(|s| {
s.favorite_models
.iter()
.map(|id| acp::ModelId::new(id.clone()))
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
update_settings_file(fs, cx, move |settings, _| {
let favorite_models = &mut settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.favorite_models;
let model_id_str = model_id.to_string();
if should_be_favorite {
if !favorite_models.contains(&model_id_str) {
favorite_models.push(model_id_str);
}
} else {
favorite_models.retain(|id| id != &model_id_str);
}
});
}
fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.and_then(|s| s.default_config_options.get(config_id).cloned())
}
fn set_default_config_option(
&self,
config_id: &str,
value_id: Option<&str>,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
let config_id = config_id.to_string();
let value_id = value_id.map(|s| s.to_string());
update_settings_file(fs, cx, move |settings, _| {
let config_options = &mut settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.default_config_options;
if let Some(value) = value_id.clone() {
config_options.insert(config_id.clone(), value);
} else {
config_options.remove(&config_id);
}
});
}
fn favorite_config_option_value_ids(
&self,
config_id: &acp::SessionConfigId,
cx: &mut App,
) -> HashSet<acp::SessionConfigValueId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
.map(|values| {
values
.iter()
.cloned()
.map(acp::SessionConfigValueId::new)
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_config_option_value(
&self,
config_id: acp::SessionConfigId,
value_id: acp::SessionConfigValueId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let config_id = config_id.to_string();
let value_id = value_id.to_string();
update_settings_file(fs, cx, move |settings, _| {
let favorites = &mut settings
.agent_servers
.get_or_insert_default()
.claude
.get_or_insert_default()
.favorite_config_option_values;
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
if should_be_favorite {
if !entry.iter().any(|v| v == &value_id) {
entry.push(value_id.clone());
}
} else {
entry.retain(|v| v != &value_id);
if entry.is_empty() {
favorites.remove(&config_id);
}
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -85,6 +219,14 @@ impl AgentServer for ClaudeCode {
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.claude
.as_ref()
.map(|s| s.default_config_options.clone())
.unwrap_or_default()
});
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -107,6 +249,7 @@ impl AgentServer for ClaudeCode {
root_dir.as_ref(),
default_mode,
default_model,
default_config_options,
is_remote,
cx,
)

View File

@@ -5,6 +5,7 @@ use std::{any::Any, path::Path};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use collections::HashSet;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
@@ -31,7 +32,7 @@ impl AgentServer for Codex {
ui::IconName::AiOpenAi
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
@@ -52,7 +53,7 @@ impl AgentServer for Codex {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
@@ -73,6 +74,139 @@ impl AgentServer for Codex {
});
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.map(|s| {
s.favorite_models
.iter()
.map(|id| acp::ModelId::new(id.clone()))
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
update_settings_file(fs, cx, move |settings, _| {
let favorite_models = &mut settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.favorite_models;
let model_id_str = model_id.to_string();
if should_be_favorite {
if !favorite_models.contains(&model_id_str) {
favorite_models.push(model_id_str);
}
} else {
favorite_models.retain(|id| id != &model_id_str);
}
});
}
fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.default_config_options.get(config_id).cloned())
}
fn set_default_config_option(
&self,
config_id: &str,
value_id: Option<&str>,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
let config_id = config_id.to_string();
let value_id = value_id.map(|s| s.to_string());
update_settings_file(fs, cx, move |settings, _| {
let config_options = &mut settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.default_config_options;
if let Some(value) = value_id.clone() {
config_options.insert(config_id.clone(), value);
} else {
config_options.remove(&config_id);
}
});
}
fn favorite_config_option_value_ids(
&self,
config_id: &acp::SessionConfigId,
cx: &mut App,
) -> HashSet<acp::SessionConfigValueId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).codex.clone()
});
settings
.as_ref()
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
.map(|values| {
values
.iter()
.cloned()
.map(acp::SessionConfigValueId::new)
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_config_option_value(
&self,
config_id: acp::SessionConfigId,
value_id: acp::SessionConfigValueId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let config_id = config_id.to_string();
let value_id = value_id.to_string();
update_settings_file(fs, cx, move |settings, _| {
let favorites = &mut settings
.agent_servers
.get_or_insert_default()
.codex
.get_or_insert_default()
.favorite_config_option_values;
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
if should_be_favorite {
if !entry.iter().any(|v| v == &value_id) {
entry.push(value_id.clone());
}
} else {
entry.retain(|v| v != &value_id);
if entry.is_empty() {
favorites.remove(&config_id);
}
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -86,6 +220,14 @@ impl AgentServer for Codex {
let extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.codex
.as_ref()
.map(|s| s.default_config_options.clone())
.unwrap_or_default()
});
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@@ -109,6 +251,7 @@ impl AgentServer for Codex {
root_dir.as_ref(),
default_mode,
default_model,
default_config_options,
is_remote,
cx,
)

View File

@@ -2,6 +2,7 @@ use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use collections::HashSet;
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
@@ -29,7 +30,7 @@ impl AgentServer for CustomAgentServer {
IconName::Terminal
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
@@ -43,6 +44,86 @@ impl AgentServer for CustomAgentServer {
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
}
fn favorite_config_option_value_ids(
&self,
config_id: &acp::SessionConfigId,
cx: &mut App,
) -> HashSet<acp::SessionConfigValueId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.favorite_config_option_values(config_id.0.as_ref()))
.map(|values| {
values
.iter()
.cloned()
.map(acp::SessionConfigValueId::new)
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_config_option_value(
&self,
config_id: acp::SessionConfigId,
value_id: acp::SessionConfigValueId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let name = self.name();
let config_id = config_id.to_string();
let value_id = value_id.to_string();
update_settings_file(fs, cx, move |settings, _| {
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
default_config_options: Default::default(),
favorite_config_option_values: Default::default(),
});
match settings {
settings::CustomAgentServerSettings::Custom {
favorite_config_option_values,
..
}
| settings::CustomAgentServerSettings::Extension {
favorite_config_option_values,
..
} => {
let entry = favorite_config_option_values
.entry(config_id.clone())
.or_insert_with(Vec::new);
if should_be_favorite {
if !entry.iter().any(|v| v == &value_id) {
entry.push(value_id.clone());
}
} else {
entry.retain(|v| v != &value_id);
if entry.is_empty() {
favorite_config_option_values.remove(&config_id);
}
}
}
}
});
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
@@ -54,6 +135,9 @@ impl AgentServer for CustomAgentServer {
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
default_config_options: Default::default(),
favorite_config_option_values: Default::default(),
});
match settings {
@@ -65,7 +149,7 @@ impl AgentServer for CustomAgentServer {
});
}
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
@@ -90,6 +174,9 @@ impl AgentServer for CustomAgentServer {
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
default_config_options: Default::default(),
favorite_config_option_values: Default::default(),
});
match settings {
@@ -101,6 +188,125 @@ impl AgentServer for CustomAgentServer {
});
}
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.map(|s| {
s.favorite_models()
.iter()
.map(|id| acp::ModelId::new(id.clone()))
.collect()
})
.unwrap_or_default()
}
fn toggle_favorite_model(
&self,
model_id: acp::ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
default_config_options: Default::default(),
favorite_config_option_values: Default::default(),
});
let favorite_models = match settings {
settings::CustomAgentServerSettings::Custom {
favorite_models, ..
}
| settings::CustomAgentServerSettings::Extension {
favorite_models, ..
} => favorite_models,
};
let model_id_str = model_id.to_string();
if should_be_favorite {
if !favorite_models.contains(&model_id_str) {
favorite_models.push(model_id_str);
}
} else {
favorite_models.retain(|id| id != &model_id_str);
}
});
}
fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_config_option(config_id).map(|s| s.to_string()))
}
fn set_default_config_option(
&self,
config_id: &str,
value_id: Option<&str>,
fs: Arc<dyn Fs>,
cx: &mut App,
) {
let name = self.name();
let config_id = config_id.to_string();
let value_id = value_id.map(|s| s.to_string());
update_settings_file(fs, cx, move |settings, _| {
let settings = settings
.agent_servers
.get_or_insert_default()
.custom
.entry(name.clone())
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
default_model: None,
default_mode: None,
favorite_models: Vec::new(),
default_config_options: Default::default(),
favorite_config_option_values: Default::default(),
});
match settings {
settings::CustomAgentServerSettings::Custom {
default_config_options,
..
}
| settings::CustomAgentServerSettings::Extension {
default_config_options,
..
} => {
if let Some(value) = value_id.clone() {
default_config_options.insert(config_id.clone(), value);
} else {
default_config_options.remove(&config_id);
}
}
}
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@@ -112,6 +318,23 @@ impl AgentServer for CustomAgentServer {
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.map(|s| match s {
project::agent_server_store::CustomAgentServerSettings::Custom {
default_config_options,
..
}
| project::agent_server_store::CustomAgentServerSettings::Extension {
default_config_options,
..
} => default_config_options.clone(),
})
.unwrap_or_default()
});
let store = delegate.store.downgrade();
let extra_env = load_proxy_env(cx);
cx.spawn(async move |cx| {
@@ -137,6 +360,7 @@ impl AgentServer for CustomAgentServer {
root_dir.as_ref(),
default_mode,
default_model,
default_config_options,
is_remote,
cx,
)

View File

@@ -455,20 +455,12 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
project::agent_server_store::AllAgentServersSettings {
claude: Some(BuiltinAgentServerSettings {
path: Some("claude-code-acp".into()),
args: None,
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
..Default::default()
}),
gemini: Some(crate::gemini::tests::local_command().into()),
codex: Some(BuiltinAgentServerSettings {
path: Some("codex-acp".into()),
args: None,
env: None,
ignore_system_version: None,
default_mode: None,
default_model: None,
..Default::default()
}),
custom: collections::HashMap::default(),
},

View File

@@ -4,9 +4,10 @@ use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::agent_server_store::GEMINI_NAME;
use project::agent_server_store::{AllAgentServersSettings, GEMINI_NAME};
use settings::SettingsStore;
#[derive(Clone)]
pub struct Gemini;
@@ -33,6 +34,14 @@ impl AgentServer for Gemini {
let mut extra_env = load_proxy_env(cx);
let default_mode = self.default_mode(cx);
let default_model = self.default_model(cx);
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.gemini
.as_ref()
.map(|s| s.default_config_options.clone())
.unwrap_or_default()
});
cx.spawn(async move |cx| {
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
@@ -65,6 +74,7 @@ impl AgentServer for Gemini {
root_dir.as_ref(),
default_mode,
default_model,
default_config_options,
is_remote,
cx,
)

View File

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

View File

@@ -0,0 +1,775 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::AgentSessionConfigOptions;
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use collections::HashSet;
use fs::Fs;
use fuzzy::StringMatchCandidate;
use gpui::{
BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,
};
use ordered_float::OrderedFloat;
use picker::popover_menu::PickerPopoverMenu;
use picker::{Picker, PickerDelegate};
use settings::SettingsStore;
use ui::{
ElevationIndex, IconButton, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,
};
use util::ResultExt as _;
use crate::ui::HoldForDefault;
const PICKER_THRESHOLD: usize = 5;
pub struct ConfigOptionsView {
config_options: Rc<dyn AgentSessionConfigOptions>,
selectors: Vec<Entity<ConfigOptionSelector>>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
config_option_ids: Vec<acp::SessionConfigId>,
_refresh_task: Task<()>,
}
impl ConfigOptionsView {
pub fn new(
config_options: Rc<dyn AgentSessionConfigOptions>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
let config_option_ids = Self::config_option_ids(&config_options);
let rx = config_options.watch(cx);
let refresh_task = cx.spawn_in(window, async move |this, cx| {
if let Some(mut rx) = rx {
while let Ok(()) = rx.recv().await {
this.update_in(cx, |this, window, cx| {
this.refresh_selectors_if_needed(window, cx);
cx.notify();
})
.log_err();
}
}
});
Self {
config_options,
selectors,
agent_server,
fs,
config_option_ids,
_refresh_task: refresh_task,
}
}
fn config_option_ids(
config_options: &Rc<dyn AgentSessionConfigOptions>,
) -> Vec<acp::SessionConfigId> {
config_options
.config_options()
.into_iter()
.map(|option| option.id)
.collect()
}
fn refresh_selectors_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let current_ids = Self::config_option_ids(&self.config_options);
if current_ids != self.config_option_ids {
self.config_option_ids = current_ids;
self.rebuild_selectors(window, cx);
}
}
fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.selectors = Self::build_selectors(
&self.config_options,
&self.agent_server,
&self.fs,
window,
cx,
);
cx.notify();
}
fn build_selectors(
config_options: &Rc<dyn AgentSessionConfigOptions>,
agent_server: &Rc<dyn AgentServer>,
fs: &Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Vec<Entity<ConfigOptionSelector>> {
config_options
.config_options()
.into_iter()
.map(|option| {
let config_options = config_options.clone();
let agent_server = agent_server.clone();
let fs = fs.clone();
cx.new(|cx| {
ConfigOptionSelector::new(
config_options,
option.id.clone(),
agent_server,
fs,
window,
cx,
)
})
})
.collect()
}
}
impl Render for ConfigOptionsView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
if self.selectors.is_empty() {
return div().into_any_element();
}
h_flex()
.gap_1()
.children(self.selectors.iter().cloned())
.into_any_element()
}
}
struct ConfigOptionSelector {
config_options: Rc<dyn AgentSessionConfigOptions>,
config_id: acp::SessionConfigId,
picker_handle: PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>,
picker: Entity<Picker<ConfigOptionPickerDelegate>>,
setting_value: bool,
}
impl ConfigOptionSelector {
pub fn new(
config_options: Rc<dyn AgentSessionConfigOptions>,
config_id: acp::SessionConfigId,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let option_count = config_options
.config_options()
.iter()
.find(|opt| opt.id == config_id)
.map(count_config_options)
.unwrap_or(0);
let is_searchable = option_count >= PICKER_THRESHOLD;
let picker = {
let config_options = config_options.clone();
let config_id = config_id.clone();
let agent_server = agent_server.clone();
let fs = fs.clone();
cx.new(move |picker_cx| {
let delegate = ConfigOptionPickerDelegate::new(
config_options,
config_id,
agent_server,
fs,
window,
picker_cx,
);
if is_searchable {
Picker::list(delegate, window, picker_cx)
} else {
Picker::nonsearchable_list(delegate, window, picker_cx)
}
.show_scrollbar(true)
.width(rems(20.))
.max_height(Some(rems(20.).into()))
})
};
Self {
config_options,
config_id,
picker_handle: PopoverMenuHandle::default(),
picker,
setting_value: false,
}
}
fn current_option(&self) -> Option<acp::SessionConfigOption> {
self.config_options
.config_options()
.into_iter()
.find(|opt| opt.id == self.config_id)
}
fn current_value_name(&self) -> String {
let Some(option) = self.current_option() else {
return "Unknown".to_string();
};
match &option.kind {
acp::SessionConfigKind::Select(select) => {
find_option_name(&select.options, &select.current_value)
.unwrap_or_else(|| "Unknown".to_string())
}
_ => "Unknown".to_string(),
}
}
fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
let Some(option) = self.current_option() else {
return Button::new("config-option-trigger", "Unknown")
.label_size(LabelSize::Small)
.color(Color::Muted)
.disabled(true);
};
let icon = if self.picker_handle.is_deployed() {
IconName::ChevronUp
} else {
IconName::ChevronDown
};
Button::new(
ElementId::Name(format!("config-option-{}", option.id.0).into()),
self.current_value_name(),
)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(icon)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.disabled(self.setting_value)
}
}
impl Render for ConfigOptionSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(option) = self.current_option() else {
return div().into_any_element();
};
let trigger_button = self.render_trigger_button(window, cx);
let option_name = option.name.clone();
let option_description: Option<SharedString> = option.description.map(Into::into);
let tooltip = Tooltip::element(move |_window, _cx| {
let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
if let Some(desc) = option_description.as_ref() {
content = content.child(
Label::new(desc.clone())
.size(LabelSize::Small)
.color(Color::Muted),
);
}
content.into_any()
});
PickerPopoverMenu::new(
self.picker.clone(),
trigger_button,
tooltip,
gpui::Corner::BottomRight,
cx,
)
.with_handle(self.picker_handle.clone())
.render(window, cx)
.into_any_element()
}
}
#[derive(Clone)]
enum ConfigOptionPickerEntry {
Separator(SharedString),
Option(ConfigOptionValue),
}
#[derive(Clone)]
struct ConfigOptionValue {
value: acp::SessionConfigValueId,
name: String,
description: Option<String>,
group: Option<String>,
}
struct ConfigOptionPickerDelegate {
config_options: Rc<dyn AgentSessionConfigOptions>,
config_id: acp::SessionConfigId,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
filtered_entries: Vec<ConfigOptionPickerEntry>,
all_options: Vec<ConfigOptionValue>,
selected_index: usize,
selected_description: Option<(usize, SharedString, bool)>,
favorites: HashSet<acp::SessionConfigValueId>,
_settings_subscription: Subscription,
}
impl ConfigOptionPickerDelegate {
fn new(
config_options: Rc<dyn AgentSessionConfigOptions>,
config_id: acp::SessionConfigId,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Self {
let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
let all_options = extract_options(&config_options, &config_id);
let filtered_entries = options_to_picker_entries(&all_options, &favorites);
let current_value = get_current_value(&config_options, &config_id);
let selected_index = current_value
.and_then(|current| {
filtered_entries.iter().position(|entry| {
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
})
})
.unwrap_or(0);
let agent_server_for_subscription = agent_server.clone();
let config_id_for_subscription = config_id.clone();
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
let new_favorites = agent_server_for_subscription
.favorite_config_option_value_ids(&config_id_for_subscription, cx);
if new_favorites != picker.delegate.favorites {
picker.delegate.favorites = new_favorites;
picker.refresh(window, cx);
}
});
cx.notify();
Self {
config_options,
config_id,
agent_server,
fs,
filtered_entries,
all_options,
selected_index,
selected_description: None,
favorites,
_settings_subscription: settings_subscription,
}
}
fn current_value(&self) -> Option<acp::SessionConfigValueId> {
get_current_value(&self.config_options, &self.config_id)
}
}
impl PickerDelegate for ConfigOptionPickerDelegate {
type ListItem = AnyElement;
fn match_count(&self) -> usize {
self.filtered_entries.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
cx.notify();
}
fn can_select(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> bool {
match self.filtered_entries.get(ix) {
Some(ConfigOptionPickerEntry::Option(_)) => true,
Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
}
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select an option…".into()
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let all_options = self.all_options.clone();
cx.spawn_in(window, async move |this, cx| {
let filtered_options = match this
.read_with(cx, |_, cx| {
if query.is_empty() {
None
} else {
Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
}
})
.ok()
.flatten()
{
Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
None => all_options,
};
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
options_to_picker_entries(&filtered_options, &this.delegate.favorites);
let current_value = this.delegate.current_value();
let new_index = current_value
.and_then(|current| {
this.delegate.filtered_entries.iter().position(|entry| {
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
})
})
.unwrap_or(0);
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if let Some(ConfigOptionPickerEntry::Option(option)) =
self.filtered_entries.get(self.selected_index)
{
if window.modifiers().secondary() {
let default_value = self
.agent_server
.default_config_option(self.config_id.0.as_ref(), cx);
let is_default = default_value.as_deref() == Some(&*option.value.0);
self.agent_server.set_default_config_option(
self.config_id.0.as_ref(),
if is_default {
None
} else {
Some(option.value.0.as_ref())
},
self.fs.clone(),
cx,
);
}
let task = self.config_options.set_config_option(
self.config_id.clone(),
option.value.clone(),
cx,
);
cx.spawn(async move |_, _| {
if let Err(err) = task.await {
log::error!("Failed to set config option: {:?}", err);
}
})
.detach();
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(
&self,
ix: usize,
selected: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
match self.filtered_entries.get(ix)? {
ConfigOptionPickerEntry::Separator(title) => Some(
div()
.when(ix > 0, |this| this.mt_1())
.child(
div()
.px_2()
.py_1()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(title.clone()),
)
.into_any_element(),
),
ConfigOptionPickerEntry::Option(option) => {
let current_value = self.current_value();
let is_selected = current_value.as_ref() == Some(&option.value);
let default_value = self
.agent_server
.default_config_option(self.config_id.0.as_ref(), cx);
let is_default = default_value.as_deref() == Some(&*option.value.0);
let is_favorite = self.favorites.contains(&option.value);
let option_name = option.name.clone();
let description = option.description.clone();
Some(
div()
.id(("config-option-picker-item", ix))
.when_some(description, |this, desc| {
let desc: SharedString = desc.into();
this.on_hover(cx.listener(move |menu, hovered, _, cx| {
if *hovered {
menu.delegate.selected_description =
Some((ix, desc.clone(), is_default));
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix)
{
menu.delegate.selected_description = None;
}
cx.notify();
}))
})
.child(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(h_flex().w_full().child(Label::new(option_name).truncate()))
.end_slot(div().pr_2().when(is_selected, |this| {
this.child(Icon::new(IconName::Check).color(Color::Accent))
}))
.end_hover_slot(div().pr_1p5().child({
let (icon, color, tooltip) = if is_favorite {
(IconName::StarFilled, Color::Accent, "Unfavorite")
} else {
(IconName::Star, Color::Default, "Favorite")
};
let config_id = self.config_id.clone();
let value_id = option.value.clone();
let agent_server = self.agent_server.clone();
let fs = self.fs.clone();
IconButton::new(("toggle-favorite-config-option", ix), icon)
.layer(ElevationIndex::ElevatedSurface)
.icon_color(color)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(tooltip))
.on_click(move |_, _, cx| {
agent_server.toggle_favorite_config_option_value(
config_id.clone(),
value_id.clone(),
!is_favorite,
fs.clone(),
cx,
);
})
})),
)
.into_any_element(),
)
}
}
}
fn documentation_aside(
&self,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<ui::DocumentationAside> {
self.selected_description
.as_ref()
.map(|(_, description, is_default)| {
let description = description.clone();
let is_default = *is_default;
ui::DocumentationAside::new(
ui::DocumentationSide::Left,
Rc::new(move |_| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(HoldForDefault::new(is_default))
.into_any_element()
}),
)
})
}
fn documentation_aside_index(&self) -> Option<usize> {
self.selected_description.as_ref().map(|(ix, _, _)| *ix)
}
}
fn extract_options(
config_options: &Rc<dyn AgentSessionConfigOptions>,
config_id: &acp::SessionConfigId,
) -> Vec<ConfigOptionValue> {
let Some(option) = config_options
.config_options()
.into_iter()
.find(|opt| &opt.id == config_id)
else {
return Vec::new();
};
match &option.kind {
acp::SessionConfigKind::Select(select) => match &select.options {
acp::SessionConfigSelectOptions::Ungrouped(options) => options
.iter()
.map(|opt| ConfigOptionValue {
value: opt.value.clone(),
name: opt.name.clone(),
description: opt.description.clone(),
group: None,
})
.collect(),
acp::SessionConfigSelectOptions::Grouped(groups) => groups
.iter()
.flat_map(|group| {
group.options.iter().map(|opt| ConfigOptionValue {
value: opt.value.clone(),
name: opt.name.clone(),
description: opt.description.clone(),
group: Some(group.name.clone()),
})
})
.collect(),
_ => Vec::new(),
},
_ => Vec::new(),
}
}
fn get_current_value(
config_options: &Rc<dyn AgentSessionConfigOptions>,
config_id: &acp::SessionConfigId,
) -> Option<acp::SessionConfigValueId> {
config_options
.config_options()
.into_iter()
.find(|opt| &opt.id == config_id)
.and_then(|opt| match &opt.kind {
acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
_ => None,
})
}
fn options_to_picker_entries(
options: &[ConfigOptionValue],
favorites: &HashSet<acp::SessionConfigValueId>,
) -> Vec<ConfigOptionPickerEntry> {
let mut entries = Vec::new();
let mut favorite_options = Vec::new();
for option in options {
if favorites.contains(&option.value) {
favorite_options.push(option.clone());
}
}
if !favorite_options.is_empty() {
entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
for option in favorite_options {
entries.push(ConfigOptionPickerEntry::Option(option));
}
// If the remaining list would start ungrouped (group == None), insert a separator so
// Favorites doesn't visually run into the main list.
if let Some(option) = options.first()
&& option.group.is_none()
{
entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
}
}
let mut current_group: Option<String> = None;
for option in options {
if option.group != current_group {
if let Some(group_name) = &option.group {
entries.push(ConfigOptionPickerEntry::Separator(
group_name.clone().into(),
));
}
current_group = option.group.clone();
}
entries.push(ConfigOptionPickerEntry::Option(option.clone()));
}
entries
}
async fn fuzzy_search_options(
options: Vec<ConfigOptionValue>,
query: &str,
executor: BackgroundExecutor,
) -> Vec<ConfigOptionValue> {
let candidates = options
.iter()
.enumerate()
.map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
.collect::<Vec<_>>();
let mut matches = fuzzy::match_strings(
&candidates,
query,
false,
true,
100,
&Default::default(),
executor,
)
.await;
matches.sort_unstable_by_key(|mat| {
let candidate = &candidates[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), candidate.id)
});
matches
.into_iter()
.map(|mat| options[mat.candidate_id].clone())
.collect()
}
fn find_option_name(
options: &acp::SessionConfigSelectOptions,
value_id: &acp::SessionConfigValueId,
) -> Option<String> {
match options {
acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
.iter()
.find(|o| &o.value == value_id)
.map(|o| o.name.clone()),
acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
group
.options
.iter()
.find(|o| &o.value == value_id)
.map(|o| o.name.clone())
}),
_ => None,
}
}
fn count_config_options(option: &acp::SessionConfigOption) -> usize {
match &option.kind {
acp::SessionConfigKind::Select(select) => match &select.options {
acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
acp::SessionConfigSelectOptions::Grouped(groups) => {
groups.iter().map(|g| g.options.len()).sum()
}
_ => 0,
},
_ => 0,
}
}

View File

@@ -31,10 +31,10 @@ use rope::Point;
use settings::Settings;
use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
use theme::ThemeSettings;
use ui::prelude::*;
use ui::{ContextMenu, prelude::*};
use util::{ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::Chat;
use zed_actions::agent::{Chat, PasteRaw};
pub struct MessageEditor {
mention_set: Entity<MentionSet>,
@@ -132,6 +132,21 @@ impl MessageEditor {
placement: Some(ContextMenuPlacement::Above),
});
editor.register_addon(MessageEditorAddon::new());
editor.set_custom_context_menu(|editor, _point, window, cx| {
let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
Some(ContextMenu::build(window, cx, |menu, _, _| {
menu.action("Cut", Box::new(editor::actions::Cut))
.action_disabled_when(
!has_selection,
"Copy",
Box::new(editor::actions::Copy),
)
.action("Paste", Box::new(editor::actions::Paste))
}))
});
editor
});
let mention_set =
@@ -543,6 +558,9 @@ impl MessageEditor {
}
fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -553,133 +571,127 @@ impl MessageEditor {
_ => None,
});
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
if has_file_context {
if let Some((workspace, selections)) =
self.workspace.upgrade().zip(editor_clipboard_selections)
{
let Some(first_selection) = selections.first() else {
return;
};
if let Some(file_path) = &first_selection.file_path {
// In case someone pastes selections from another window
// with a different project, we don't want to insert the
// crease (containing the absolute path) since the agent
// cannot access files outside the project.
let is_in_project = workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some();
if !is_in_project {
return;
}
}
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) =
snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path, cx)
})
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content =
buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
}
}
return;
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
if line_range.start() == line_range.end() {
return Some(false);
}
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
if should_insert_creases && let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
let insertion_target = self
.editor
.read(cx)
.selections
.newest_anchor()
.start
.text_anchor;
let project = workspace.read(cx).project().clone();
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let crease_text =
acp_thread::selection_name(Some(file_path.as_ref()), &line_range);
let mention_uri = MentionUri::Selection {
abs_path: Some(file_path.clone()),
line_range: line_range.clone(),
};
let mention_text = mention_uri.as_link().to_string();
let (excerpt_id, text_anchor, content_len) =
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx);
let snapshot = buffer.snapshot(cx);
let (excerpt_id, _, buffer_snapshot) = snapshot.as_singleton().unwrap();
let text_anchor = insertion_target.bias_left(&buffer_snapshot);
editor.insert(&mention_text, window, cx);
editor.insert(" ", window, cx);
(*excerpt_id, text_anchor, mention_text.len())
});
let Some((crease_id, tx)) = insert_crease_for_mention(
excerpt_id,
text_anchor,
content_len,
crease_text.into(),
mention_uri.icon_path(cx),
None,
self.editor.clone(),
window,
cx,
) else {
continue;
};
drop(tx);
let mention_task = cx
.spawn({
let project = project.clone();
async move |_, cx| {
let project_path = project
.update(cx, |project, cx| {
project.project_path_for_absolute_path(&file_path, cx)
})
.map_err(|e| e.to_string())?
.ok_or_else(|| "project path not found".to_string())?;
let buffer = project
.update(cx, |project, cx| project.open_buffer(project_path, cx))
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?;
buffer
.update(cx, |buffer, cx| {
let start = Point::new(*line_range.start(), 0)
.min(buffer.max_point());
let end = Point::new(*line_range.end() + 1, 0)
.min(buffer.max_point());
let content = buffer.text_for_range(start..end).collect();
Mention::Text {
content,
tracked_buffers: vec![cx.entity()],
}
})
.map_err(|e| e.to_string())
}
})
.shared();
self.mention_set.update(cx, |mention_set, _cx| {
mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task)
});
}
}
return;
}
if self.prompt_capabilities.borrow().image
@@ -690,6 +702,13 @@ impl MessageEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
let editor = self.editor.clone();
window.defer(cx, move |window, cx| {
editor.update(cx, |editor, cx| editor.paste(&Paste, window, cx));
});
}
pub fn insert_dragged_files(
&mut self,
paths: Vec<project::ProjectPath>,
@@ -967,6 +986,7 @@ impl Render for MessageEditor {
.on_action(cx.listener(Self::chat))
.on_action(cx.listener(Self::chat_with_follow))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::paste_raw))
.capture_action(cx.listener(Self::paste))
.flex_1()
.child({

View File

@@ -7,8 +7,8 @@ use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use settings::Settings as _;
use std::{rc::Rc, sync::Arc};
use ui::{
Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
Button, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
PopoverMenuHandle, Tooltip, prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
@@ -105,7 +105,7 @@ impl ModeSelector {
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
entry.documentation_aside(side, DocumentationEdge::Bottom, {
entry.documentation_aside(side, {
let description = description.clone();
move |_| {
@@ -186,6 +186,17 @@ impl Render for ModeSelector {
move |_window, cx| {
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.child(
h_flex()
.pb_1()
@@ -200,17 +211,6 @@ impl Render for ModeSelector {
cx,
)),
)
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.child(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
cx,
)),
)
.into_any()
}
}),

View File

@@ -1,22 +1,22 @@
use std::{cmp::Reverse, rc::Rc, sync::Arc};
use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
use agent_client_protocol::ModelId;
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use anyhow::Result;
use collections::{HashSet, IndexMap};
use fs::Fs;
use futures::FutureExt;
use fuzzy::{StringMatchCandidate, match_strings};
use gpui::{
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
WeakEntity,
};
use itertools::Itertools;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::Settings;
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
use settings::SettingsStore;
use ui::{DocumentationAside, DocumentationSide, IntoElement, prelude::*};
use util::ResultExt;
use zed_actions::agent::OpenSettings;
@@ -54,7 +54,9 @@ pub struct AcpModelPickerDelegate {
selected_index: usize,
selected_description: Option<(usize, SharedString, bool)>,
selected_model: Option<AgentModelInfo>,
favorites: HashSet<ModelId>,
_refresh_models_task: Task<()>,
_settings_subscription: Subscription,
focus_handle: FocusHandle,
}
@@ -102,6 +104,19 @@ impl AcpModelPickerDelegate {
})
};
let agent_server_for_subscription = agent_server.clone();
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
// Only refresh if the favorites actually changed to avoid redundant work
// when other settings are modified (e.g., user editing settings.json)
let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);
if new_favorites != picker.delegate.favorites {
picker.delegate.favorites = new_favorites;
picker.refresh(window, cx);
}
});
let favorites = agent_server.favorite_model_ids(cx);
Self {
selector,
agent_server,
@@ -111,7 +126,9 @@ impl AcpModelPickerDelegate {
selected_model: None,
selected_index: 0,
selected_description: None,
favorites,
_refresh_models_task: refresh_models_task,
_settings_subscription: settings_subscription,
focus_handle,
}
}
@@ -120,40 +137,37 @@ impl AcpModelPickerDelegate {
self.selected_model.as_ref()
}
pub fn favorites_count(&self) -> usize {
self.favorites.len()
}
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if !self.selector.supports_favorites() {
if self.favorites.is_empty() {
return;
}
let favorites = AgentSettings::get_global(cx).favorite_model_ids();
if favorites.is_empty() {
return;
}
let Some(models) = self.models.clone() else {
let Some(models) = &self.models else {
return;
};
let all_models: Vec<AgentModelInfo> = match models {
AgentModelList::Flat(list) => list,
AgentModelList::Grouped(index_map) => index_map
.into_values()
.flatten()
.collect::<Vec<AgentModelInfo>>(),
let all_models: Vec<&AgentModelInfo> = match models {
AgentModelList::Flat(list) => list.iter().collect(),
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
};
let favorite_models = all_models
.iter()
.filter(|model| favorites.contains(&model.id))
let favorite_models: Vec<_> = all_models
.into_iter()
.filter(|model| self.favorites.contains(&model.id))
.unique_by(|model| &model.id)
.cloned()
.collect::<Vec<_>>();
.collect();
let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
if favorite_models.is_empty() {
return;
}
let current_id = self.selected_model.as_ref().map(|m| &m.id);
let current_index_in_favorites = current_id
.as_ref()
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
.unwrap_or(usize::MAX);
@@ -220,11 +234,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let favorites = if self.selector.supports_favorites() {
Arc::new(AgentSettings::get_global(cx).favorite_model_ids())
} else {
Default::default()
};
let favorites = self.favorites.clone();
cx.spawn_in(window, async move |this, cx| {
let filtered_models = match this
@@ -242,7 +252,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries =
info_list_to_picker_entries(filtered_models, favorites);
info_list_to_picker_entries(filtered_models, &favorites);
// Finds the currently selected model in the list
let new_index = this
.delegate
@@ -317,21 +327,20 @@ impl PickerDelegate for AcpModelPickerDelegate {
let default_model = self.agent_server.default_model(cx);
let is_default = default_model.as_ref() == Some(&model_info.id);
let supports_favorites = self.selector.supports_favorites();
let is_favorite = *is_favorite;
let handle_action_click = {
let model_id = model_info.id.clone();
let fs = self.fs.clone();
let agent_server = self.agent_server.clone();
move |cx: &App| {
crate::favorite_models::toggle_model_id_in_settings(
cx.listener(move |_, _, _, cx| {
agent_server.toggle_favorite_model(
model_id.clone(),
!is_favorite,
fs.clone(),
cx,
);
}
})
};
Some(
@@ -350,13 +359,15 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
.child(
ModelSelectorListItem::new(ix, model_info.name.clone())
.when_some(model_info.icon, |this, icon| this.icon(icon))
.map(|this| match &model_info.icon {
Some(AgentModelIcon::Path(path)) => this.icon_path(path.clone()),
Some(AgentModelIcon::Named(icon)) => this.icon(*icon),
None => this,
})
.is_selected(is_selected)
.is_focused(selected)
.when(supports_favorites, |this| {
this.is_favorite(is_favorite)
.on_toggle_favorite(handle_action_click)
}),
.is_favorite(is_favorite)
.on_toggle_favorite(handle_action_click),
)
.into_any_element(),
)
@@ -377,7 +388,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
DocumentationAside::new(
DocumentationSide::Left,
DocumentationEdge::Top,
Rc::new(move |_| {
v_flex()
.gap_1()
@@ -389,6 +399,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
}
fn documentation_aside_index(&self) -> Option<usize> {
self.selected_description.as_ref().map(|(ix, _, _)| *ix)
}
fn render_footer(
&self,
_window: &mut Window,
@@ -406,7 +420,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
fn info_list_to_picker_entries(
model_list: AgentModelList,
favorites: Arc<HashSet<ModelId>>,
favorites: &HashSet<ModelId>,
) -> Vec<AcpModelPickerEntry> {
let mut entries = Vec::new();
@@ -572,13 +586,11 @@ mod tests {
}
}
fn create_favorites(models: Vec<&str>) -> Arc<HashSet<ModelId>> {
Arc::new(
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect(),
)
fn create_favorites(models: Vec<&str>) -> HashSet<ModelId> {
models
.into_iter()
.map(|m| ModelId::new(m.to_string()))
.collect()
}
fn get_entry_model_ids(entries: &[AcpModelPickerEntry]) -> Vec<&str> {
@@ -601,141 +613,6 @@ mod tests {
.collect()
}
#[gpui::test]
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
}
#[gpui::test]
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
));
}
#[gpui::test]
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
if info.id.0.as_ref() == "zed/claude" {
assert!(is_favorite, "zed/claude should be a favorite");
} else {
assert!(!is_favorite, "{} should not be a favorite", info.id.0);
}
}
}
}
#[gpui::test]
fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
assert_eq!(model_ids[1], "openai/gpt-5");
assert!(model_ids[2..].contains(&"zed/gemini"));
assert!(model_ids[2..].contains(&"openai/gpt-5"));
}
#[gpui::test]
fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("Recommended", vec!["zed/claude", "anthropic/claude"]),
("Zed", vec!["zed/claude", "zed/gpt-5"]),
("Antropic", vec!["anthropic/claude"]),
("OpenAI", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
labels,
vec![
"Favorite",
"zed/claude",
"Recommended",
"zed/claude",
"anthropic/claude",
"Zed",
"zed/claude",
"zed/gpt-5",
"Antropic",
"anthropic/claude",
"OpenAI",
"openai/gpt-5"
]
);
}
#[gpui::test]
fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
let models = AgentModelList::Flat(vec![
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/claude".to_string()),
name: "Claude".into(),
description: None,
icon: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/gemini".to_string()),
name: "Gemini".into(),
description: None,
icon: None,
},
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
assert!(entries.iter().any(|e| matches!(
e,
AcpModelPickerEntry::Separator(s) if s == "All"
)));
}
#[gpui::test]
async fn test_fuzzy_match(cx: &mut TestAppContext) {
let models = create_model_list(vec![
@@ -775,4 +652,185 @@ mod tests {
],
);
}
#[gpui::test]
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
}
#[gpui::test]
fn test_no_favorites_section_when_no_favorites(_cx: &mut TestAppContext) {
let models = create_model_list(vec![("zed", vec!["zed/claude", "zed/gemini"])]);
let favorites = create_favorites(vec![]);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "zed"
));
}
#[gpui::test]
fn test_models_have_correct_actions(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, &favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
if info.id.0.as_ref() == "zed/claude" {
assert!(is_favorite, "zed/claude should be a favorite");
} else {
assert!(!is_favorite, "{} should not be a favorite", info.id.0);
}
}
}
}
#[gpui::test]
fn test_favorites_appear_in_both_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("zed", vec!["zed/claude", "zed/gemini"]),
("openai", vec!["openai/gpt-5", "openai/gpt-4"]),
]);
let favorites = create_favorites(vec!["zed/gemini", "openai/gpt-5"]);
let entries = info_list_to_picker_entries(models, &favorites);
let model_ids = get_entry_model_ids(&entries);
assert_eq!(model_ids[0], "zed/gemini");
assert_eq!(model_ids[1], "openai/gpt-5");
assert!(model_ids[2..].contains(&"zed/gemini"));
assert!(model_ids[2..].contains(&"openai/gpt-5"));
}
#[gpui::test]
fn test_favorites_are_not_duplicated_when_repeated_in_other_sections(_cx: &mut TestAppContext) {
let models = create_model_list(vec![
("Recommended", vec!["zed/claude", "anthropic/claude"]),
("Zed", vec!["zed/claude", "zed/gpt-5"]),
("Antropic", vec!["anthropic/claude"]),
("OpenAI", vec!["openai/gpt-5"]),
]);
let favorites = create_favorites(vec!["zed/claude"]);
let entries = info_list_to_picker_entries(models, &favorites);
let labels = get_entry_labels(&entries);
assert_eq!(
labels,
vec![
"Favorite",
"zed/claude",
"Recommended",
"zed/claude",
"anthropic/claude",
"Zed",
"zed/claude",
"zed/gpt-5",
"Antropic",
"anthropic/claude",
"OpenAI",
"openai/gpt-5"
]
);
}
#[gpui::test]
fn test_flat_model_list_with_favorites(_cx: &mut TestAppContext) {
let models = AgentModelList::Flat(vec![
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/claude".to_string()),
name: "Claude".into(),
description: None,
icon: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/gemini".to_string()),
name: "Gemini".into(),
description: None,
icon: None,
},
]);
let favorites = create_favorites(vec!["zed/gemini"]);
let entries = info_list_to_picker_entries(models, &favorites);
assert!(matches!(
entries.first(),
Some(AcpModelPickerEntry::Separator(s)) if s == "Favorite"
));
assert!(entries.iter().any(|e| matches!(
e,
AcpModelPickerEntry::Separator(s) if s == "All"
)));
}
#[gpui::test]
fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) {
let empty_favorites: HashSet<ModelId> = HashSet::default();
assert_eq!(empty_favorites.len(), 0);
let one_favorite = create_favorites(vec!["model-a"]);
assert_eq!(one_favorite.len(), 1);
let multiple_favorites = create_favorites(vec!["model-a", "model-b", "model-c"]);
assert_eq!(multiple_favorites.len(), 3);
let with_duplicates = create_favorites(vec!["model-a", "model-a", "model-b"]);
assert_eq!(with_duplicates.len(), 2);
}
#[gpui::test]
fn test_is_favorite_flag_set_correctly_in_entries(_cx: &mut TestAppContext) {
let models = AgentModelList::Flat(vec![
acp_thread::AgentModelInfo {
id: acp::ModelId::new("favorite-model".to_string()),
name: "Favorite".into(),
description: None,
icon: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("regular-model".to_string()),
name: "Regular".into(),
description: None,
icon: None,
},
]);
let favorites = create_favorites(vec!["favorite-model"]);
let entries = info_list_to_picker_entries(models, &favorites);
for entry in &entries {
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
if info.id.0.as_ref() == "favorite-model" {
assert!(*is_favorite, "favorite-model should have is_favorite=true");
} else if info.id.0.as_ref() == "regular-model" {
assert!(!*is_favorite, "regular-model should have is_favorite=false");
}
}
}
}
}

View File

@@ -1,18 +1,14 @@
use std::rc::Rc;
use std::sync::Arc;
use acp_thread::{AgentModelInfo, AgentModelSelector};
use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
use fs::Fs;
use gpui::{Entity, FocusHandle};
use picker::popover_menu::PickerPopoverMenu;
use settings::Settings as _;
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use crate::CycleFavoriteModels;
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
use crate::ui::ModelSelectorTooltip;
pub struct AcpModelSelectorPopover {
selector: Entity<AcpModelSelector>,
@@ -23,7 +19,7 @@ pub struct AcpModelSelectorPopover {
impl AcpModelSelectorPopover {
pub(crate) fn new(
selector: Rc<dyn AgentModelSelector>,
agent_server: Rc<dyn AgentServer>,
agent_server: Rc<dyn agent_servers::AgentServer>,
fs: Arc<dyn Fs>,
menu_handle: PopoverMenuHandle<AcpModelSelector>,
focus_handle: FocusHandle,
@@ -64,13 +60,14 @@ impl AcpModelSelectorPopover {
impl Render for AcpModelSelectorPopover {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let model = self.selector.read(cx).delegate.active_model();
let selector = self.selector.read(cx);
let model = selector.delegate.active_model();
let model_name = model
.as_ref()
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("Select a Model"));
let model_icon = model.as_ref().and_then(|model| model.icon);
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
let focus_handle = self.focus_handle.clone();
@@ -80,43 +77,13 @@ impl Render for AcpModelSelectorPopover {
(Color::Muted, IconName::ChevronDown)
};
let tooltip = Tooltip::element({
move |_, cx| {
let focus_handle = focus_handle.clone();
let should_show_cycle_row = !AgentSettings::get_global(cx)
.favorite_model_ids()
.is_empty();
let show_cycle_row = selector.delegate.favorites_count() > 1;
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Change Model"))
.child(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
cx,
)),
)
.when(should_show_cycle_row, |this| {
this.child(
h_flex()
.pt_1()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(Label::new("Cycle Favorited Models"))
.child(KeyBinding::for_action_in(
&CycleFavoriteModels,
&focus_handle,
cx,
)),
)
})
.into_any()
let tooltip = Tooltip::element({
move |_, _cx| {
ModelSelectorTooltip::new(focus_handle.clone())
.show_cycle_row(show_cycle_row)
.into_any_element()
}
});
@@ -125,7 +92,14 @@ impl Render for AcpModelSelectorPopover {
ButtonLike::new("active-model")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.when_some(model_icon, |this, icon| {
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
this.child(
match icon {
AgentModelIcon::Path(path) => Icon::from_external_svg(path),
AgentModelIcon::Named(icon_name) => Icon::new(icon_name),
}
.color(color)
.size(IconSize::XSmall),
)
})
.child(
Label::new(model_name)

View File

@@ -1,7 +1,7 @@
use crate::acp::AcpThreadView;
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -402,7 +402,22 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -423,11 +438,14 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(thread_timestamp)
Label::new(display_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,8 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -117,7 +118,7 @@ impl AgentConfiguration {
}
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
for provider in providers {
self.add_provider_configuration_view(&provider, window, cx);
}
@@ -261,9 +262,12 @@ impl AgentConfiguration {
.w_full()
.gap_1p5()
.child(
Icon::new(provider.icon())
.size(IconSize::Small)
.color(Color::Muted),
match provider.icon() {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
@@ -416,7 +420,7 @@ impl AgentConfiguration {
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(
@@ -1366,6 +1370,9 @@ async fn open_new_agent_servers_entry_in_settings_editor(
env: Some(HashMap::default()),
default_mode: None,
default_model: None,
favorite_models: vec![],
default_config_options: Default::default(),
favorite_config_option_values: Default::default(),
},
);
}

View File

@@ -17,7 +17,7 @@ use gpui::{
Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*,
};
use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point};
use language::{Buffer, Capability, OffsetRangeExt, Point};
use multi_buffer::PathKey;
use project::{Project, ProjectItem, ProjectPath};
use settings::{Settings, SettingsStore};
@@ -146,13 +146,13 @@ impl AgentDiffPane {
paths_to_delete.remove(&path_key);
let snapshot = buffer.read(cx).snapshot();
let diff = diff_handle.read(cx);
let diff_hunk_ranges = diff
let diff_hunk_ranges = diff_handle
.read(cx)
.snapshot(cx)
.hunks_intersecting_range(
language::Anchor::min_max_range_for_buffer(snapshot.remote_id()),
&snapshot,
cx,
)
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
.collect::<Vec<_>>();
@@ -192,7 +192,7 @@ impl AgentDiffPane {
&& buffer
.read(cx)
.file()
.is_some_and(|file| file.disk_state() == DiskState::Deleted)
.is_some_and(|file| file.disk_state().is_deleted())
{
editor.fold_buffer(snapshot.text.remote_id(), cx)
}
@@ -1363,7 +1363,8 @@ impl AgentDiff {
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::AvailableCommandsUpdated(_)
| AcpThreadEvent::Retry(_)
| AcpThreadEvent::ModeUpdated(_) => {}
| AcpThreadEvent::ModeUpdated(_)
| AcpThreadEvent::ConfigOptionsUpdated(_) => {}
}
}

View File

@@ -1,14 +1,15 @@
use crate::{
ModelUsageContext,
language_model_selector::{LanguageModelSelector, language_model_selector},
ui::ModelSelectorTooltip,
};
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use language_model::IconOrSvg;
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
@@ -80,6 +81,12 @@ impl AgentModelSelector {
pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
self.selector.read(cx).delegate.active_model(cx)
}
pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
self.selector.update(cx, |selector, cx| {
selector.delegate.cycle_favorite_models(window, cx);
});
}
}
impl Render for AgentModelSelector {
@@ -97,13 +104,30 @@ impl Render for AgentModelSelector {
Color::Muted
};
let show_cycle_row = self.selector.read(cx).delegate.favorites_count() > 1;
let focus_handle = self.focus_handle.clone();
let tooltip = Tooltip::element({
move |_, _cx| {
ModelSelectorTooltip::new(focus_handle.clone())
.show_cycle_row(show_cycle_row)
.into_any_element()
}
});
PickerPopoverMenu::new(
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(provider_icon, |this, icon| {
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
this.child(
match icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall),
)
})
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
@@ -115,11 +139,9 @@ impl Render for AgentModelSelector {
.child(
Icon::new(IconName::ChevronDown)
.color(color)
.size(IconSize::Small),
.size(IconSize::XSmall),
),
move |_window, cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
},
tooltip,
gpui::Corner::TopRight,
cx,
)

View File

@@ -2428,7 +2428,7 @@ impl AgentPanel {
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.any(|provider| {
provider.is_authenticated(cx)

View File

@@ -348,7 +348,8 @@ fn init_language_model_settings(cx: &mut App) {
|_, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
update_active_language_model_from_settings(cx);
}
_ => {}

View File

@@ -1586,7 +1586,7 @@ pub(crate) fn search_rules(
None
} else {
Some(RulesContextEntry {
prompt_id: metadata.id.user_id()?,
prompt_id: metadata.id.as_user()?,
title: metadata.title?,
})
}

View File

@@ -1,6 +1,5 @@
use std::sync::Arc;
use agent_client_protocol::ModelId;
use fs::Fs;
use language_model::LanguageModel;
use settings::{LanguageModelSelection, update_settings_file};
@@ -13,20 +12,11 @@ fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelS
}
}
fn model_id_to_selection(model_id: &ModelId) -> LanguageModelSelection {
let id = model_id.0.as_ref();
let (provider, model) = id.split_once('/').unwrap_or(("", id));
LanguageModelSelection {
provider: provider.to_owned().into(),
model: model.to_owned(),
}
}
pub fn toggle_in_settings(
model: Arc<dyn LanguageModel>,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
cx: &mut App,
) {
let selection = language_model_to_selection(&model);
update_settings_file(fs, cx, move |settings, _| {
@@ -38,20 +28,3 @@ pub fn toggle_in_settings(
}
});
}
pub fn toggle_model_id_in_settings(
model_id: ModelId,
should_be_favorite: bool,
fs: Arc<dyn Fs>,
cx: &App,
) {
let selection = model_id_to_selection(&model_id);
update_settings_file(fs, cx, move |settings, _| {
let agent = settings.agent.get_or_insert_default();
if should_be_favorite {
agent.add_favorite_model(selection.clone());
} else {
agent.remove_favorite_model(&selection);
}
});
}

View File

@@ -1259,28 +1259,26 @@ impl InlineAssistant {
let bottom = top + 1.0;
(top, bottom)
});
let mut scroll_target_top = scroll_target_range.0;
let mut scroll_target_bottom = scroll_target_range.1;
scroll_target_top -= editor.vertical_scroll_margin() as ScrollOffset;
scroll_target_bottom += editor.vertical_scroll_margin() as ScrollOffset;
let height_in_lines = editor.visible_line_count().unwrap_or(0.);
let vertical_scroll_margin = editor.vertical_scroll_margin() as ScrollOffset;
let scroll_target_top = (scroll_target_range.0 - vertical_scroll_margin)
// Don't scroll up too far in the case of a large vertical_scroll_margin.
.max(scroll_target_range.0 - height_in_lines / 2.0);
let scroll_target_bottom = (scroll_target_range.1 + vertical_scroll_margin)
// Don't scroll down past where the top would still be visible.
.min(scroll_target_top + height_in_lines);
let scroll_top = editor.scroll_position(cx).y;
let scroll_bottom = scroll_top + height_in_lines;
if scroll_target_top < scroll_top {
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
} else if scroll_target_bottom > scroll_bottom {
if (scroll_target_bottom - scroll_target_top) <= height_in_lines {
editor.set_scroll_position(
point(0., scroll_target_bottom - height_in_lines),
window,
cx,
);
} else {
editor.set_scroll_position(point(0., scroll_target_top), window, cx);
}
editor.set_scroll_position(
point(0., scroll_target_bottom - height_in_lines),
window,
cx,
);
}
});
}

View File

@@ -40,7 +40,9 @@ use crate::completion_provider::{
use crate::mention_set::paste_images_as_context;
use crate::mention_set::{MentionSet, crease_for_mention};
use crate::terminal_codegen::TerminalCodegen;
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
use crate::{
CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
};
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
@@ -148,7 +150,7 @@ impl<T: 'static> Render for PromptEditor<T> {
.into_any_element();
v_flex()
.key_context("PromptEditor")
.key_context("InlineAssistant")
.capture_action(cx.listener(Self::paste))
.block_mouse_except_scroll()
.size_full()
@@ -162,10 +164,6 @@ impl<T: 'static> Render for PromptEditor<T> {
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::move_up))
@@ -174,6 +172,15 @@ impl<T: 'static> Render for PromptEditor<T> {
.on_action(cx.listener(Self::thumbs_down))
.capture_action(cx.listener(Self::cycle_prev))
.capture_action(cx.listener(Self::cycle_next))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
this.model_selector
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
}))
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
this.model_selector.update(cx, |model_selector, cx| {
model_selector.cycle_favorite_models(window, cx);
});
}))
.child(
WithRemSize::new(ui_font_size)
.h_full()
@@ -855,7 +862,7 @@ impl<T: 'static> PromptEditor<T> {
.map(|this| {
if rated {
this.disabled(true)
.icon_color(Color::Ignored)
.icon_color(Color::Disabled)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Good Result",
@@ -865,8 +872,15 @@ impl<T: 'static> PromptEditor<T> {
)
})
} else {
this.icon_color(Color::Muted)
.tooltip(Tooltip::text("Good Result"))
this.icon_color(Color::Muted).tooltip(
move |_, cx| {
Tooltip::for_action(
"Good Result",
&ThumbsUpResult,
cx,
)
},
)
}
})
.on_click(cx.listener(|this, _, window, cx| {
@@ -879,7 +893,7 @@ impl<T: 'static> PromptEditor<T> {
.map(|this| {
if rated {
this.disabled(true)
.icon_color(Color::Ignored)
.icon_color(Color::Disabled)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Bad Result",
@@ -889,8 +903,15 @@ impl<T: 'static> PromptEditor<T> {
)
})
} else {
this.icon_color(Color::Muted)
.tooltip(Tooltip::text("Bad Result"))
this.icon_color(Color::Muted).tooltip(
move |_, cx| {
Tooltip::for_action(
"Bad Result",
&ThumbsDownResult,
cx,
)
},
)
}
})
.on_click(cx.listener(|this, _, window, cx| {
@@ -1088,7 +1109,6 @@ impl<T: 'static> PromptEditor<T> {
let colors = cx.theme().colors();
div()
.key_context("InlineAssistEditor")
.size_full()
.p_2()
.pl_1()

View File

@@ -7,8 +7,8 @@ use gpui::{
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
};
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
LanguageModelProviderId, LanguageModelRegistry,
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -20,14 +20,14 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static>;
pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -55,7 +55,7 @@ pub fn language_model_selector(
fn all_models(cx: &App) -> GroupedModels {
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
let providers = lm_registry.providers();
let providers = lm_registry.visible_providers();
let mut favorites_index = FavoritesIndex::default();
@@ -94,7 +94,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
icon: IconOrSvg,
is_favorite: bool,
}
@@ -133,7 +133,7 @@ impl LanguageModelPickerDelegate {
fn new(
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
popover_styles: bool,
focus_handle: FocusHandle,
window: &mut Window,
@@ -203,7 +203,7 @@ impl LanguageModelPickerDelegate {
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -250,6 +250,10 @@ impl LanguageModelPickerDelegate {
(self.get_active_model)(cx)
}
pub fn favorites_count(&self) -> usize {
self.all_models.favorites.len()
}
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if self.all_models.favorites.is_empty() {
return;
@@ -474,7 +478,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let configured_providers = language_model_registry
.read(cx)
.providers()
.visible_providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -561,12 +565,18 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let handle_action_click = {
let model = model_info.model.clone();
let on_toggle_favorite = self.on_toggle_favorite.clone();
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
cx.listener(move |picker, _, window, cx| {
on_toggle_favorite(model.clone(), !is_favorite, cx);
picker.refresh(window, cx);
})
};
Some(
ModelSelectorListItem::new(ix, model_info.model.name().0)
.icon(model_info.icon)
.map(|this| match &model_info.icon {
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
})
.is_selected(is_selected)
.is_focused(selected)
.is_favorite(is_favorite)
@@ -702,7 +712,7 @@ mod tests {
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
ModelInfo {
model: Arc::new(TestLanguageModel::new(name, provider)),
icon: IconName::Ai,
icon: IconOrSvg::Icon(IconName::Ai),
is_favorite,
}
})

View File

@@ -12,8 +12,8 @@ use editor::{
};
use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
use gpui::{
Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
Image, ImageFormat, Img, SharedString, Task, WeakEntity, pulsating_between,
AppContext, ClipboardEntry, Context, Empty, Entity, EntityId, Image, ImageFormat, Img,
SharedString, Task, WeakEntity,
};
use http_client::{AsyncBody, HttpClientWithUrl};
use itertools::Either;
@@ -32,13 +32,14 @@ use std::{
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
time::Duration,
};
use text::OffsetRangeExt;
use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
use ui::{Disclosure, Toggleable, prelude::*};
use util::{ResultExt, debug_panic, rel_path::RelPath};
use workspace::{Workspace, notifications::NotifyResultExt as _};
use crate::ui::MentionCrease;
pub type MentionTask = Shared<Task<Result<Mention, String>>>;
#[derive(Debug, Clone, Eq, PartialEq)]
@@ -754,25 +755,8 @@ fn render_fold_icon_button(
.update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
.unwrap_or_default();
ButtonLike::new(fold_id)
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(icon_path.clone())
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
),
)
MentionCrease::new(fold_id, icon_path.clone(), label.clone())
.is_toggled(is_in_text_selection)
.into_any_element()
}
})
@@ -947,12 +931,14 @@ impl Render for LoadingContext {
.editor
.update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
.unwrap_or_default();
ButtonLike::new(("loading-context", self.id))
.style(ButtonStyle::Filled)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(is_in_text_selection)
.when_some(self.image.clone(), |el, image_task| {
el.hoverable_tooltip(move |_, cx| {
let id = ElementId::from(("loading_context", self.id));
MentionCrease::new(id, self.icon.clone(), self.label.clone())
.is_toggled(is_in_text_selection)
.is_loading(self.loading.is_some())
.when_some(self.image.clone(), |this, image_task| {
this.image_preview(move |_, cx| {
let image = image_task.peek().cloned().transpose().ok().flatten();
let image_task = image_task.clone();
cx.new::<ImageHover>(|cx| ImageHover {
@@ -971,35 +957,6 @@ impl Render for LoadingContext {
.into()
})
})
.child(
h_flex()
.gap_1()
.child(
Icon::from_path(self.icon.clone())
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(self.label.clone())
.size(LabelSize::Small)
.buffer_font(cx)
.single_line(),
)
.map(|el| {
if self.loading.is_some() {
el.with_animation(
"loading-context-crease",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any()
} else {
el.into_any()
}
}),
)
}
}

View File

@@ -15,8 +15,8 @@ use std::{
sync::{Arc, atomic::AtomicBool},
};
use ui::{
DocumentationAside, DocumentationEdge, DocumentationSide, HighlightedLabel, KeyBinding,
LabelSize, ListItem, ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
DocumentationAside, DocumentationSide, HighlightedLabel, KeyBinding, LabelSize, ListItem,
ListItemSpacing, PopoverMenuHandle, TintColor, Tooltip, prelude::*,
};
/// Trait for types that can provide and manage agent profiles
@@ -191,6 +191,9 @@ impl Render for ProfileSelector {
let container = || h_flex().gap_1().justify_between();
v_flex()
.gap_1()
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.child(
container()
.pb_1()
@@ -203,9 +206,6 @@ impl Render for ProfileSelector {
cx,
)),
)
.child(container().child(Label::new("Toggle Profile Menu")).child(
KeyBinding::for_action_in(&ToggleProfileSelector, &focus_handle, cx),
))
.into_any()
}
}),
@@ -244,6 +244,7 @@ pub(crate) struct ProfilePickerDelegate {
string_candidates: Arc<Vec<StringMatchCandidate>>,
filtered_entries: Vec<ProfilePickerEntry>,
selected_index: usize,
hovered_index: Option<usize>,
query: String,
cancel: Option<Arc<AtomicBool>>,
focus_handle: FocusHandle,
@@ -270,6 +271,7 @@ impl ProfilePickerDelegate {
string_candidates,
filtered_entries,
selected_index: 0,
hovered_index: None,
query: String::new(),
cancel: None,
focus_handle,
@@ -578,23 +580,38 @@ impl PickerDelegate for ProfilePickerDelegate {
let candidate = self.candidates.get(entry.candidate_index)?;
let active_id = self.provider.profile_id(cx);
let is_active = active_id == candidate.id;
let has_documentation = Self::documentation(candidate).is_some();
Some(
ListItem::new(candidate.id.0.clone())
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
candidate.name.clone(),
entry.positions.clone(),
))
.when(is_active, |this| {
this.end_slot(
div()
.pr_2()
.child(Icon::new(IconName::Check).color(Color::Accent)),
)
div()
.id(("profile-picker-item", ix))
.when(has_documentation, |this| {
this.on_hover(cx.listener(move |picker, hovered, _, cx| {
if *hovered {
picker.delegate.hovered_index = Some(ix);
} else if picker.delegate.hovered_index == Some(ix) {
picker.delegate.hovered_index = None;
}
cx.notify();
}))
})
.child(
ListItem::new(candidate.id.0.clone())
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
candidate.name.clone(),
entry.positions.clone(),
))
.when(is_active, |this| {
this.end_slot(
div()
.pr_2()
.child(Icon::new(IconName::Check).color(Color::Accent)),
)
}),
)
.into_any_element(),
)
}
@@ -608,7 +625,8 @@ impl PickerDelegate for ProfilePickerDelegate {
) -> Option<DocumentationAside> {
use std::rc::Rc;
let entry = match self.filtered_entries.get(self.selected_index)? {
let hovered_index = self.hovered_index?;
let entry = match self.filtered_entries.get(hovered_index)? {
ProfilePickerEntry::Profile(entry) => entry,
ProfilePickerEntry::Header(_) => return None,
};
@@ -626,11 +644,14 @@ impl PickerDelegate for ProfilePickerDelegate {
Some(DocumentationAside {
side,
edge: DocumentationEdge::Top,
render: Rc::new(move |_| Label::new(docs_aside.clone()).into_any_element()),
})
}
fn documentation_aside_index(&self) -> Option<usize> {
self.hovered_index
}
fn render_footer(
&self,
_: &mut Window,
@@ -718,6 +739,7 @@ mod tests {
string_candidates: Arc::new(Vec::new()),
filtered_entries: Vec::new(),
selected_index: 0,
hovered_index: None,
query: String::new(),
cancel: None,
focus_handle,
@@ -752,6 +774,7 @@ mod tests {
background: cx.background_executor().clone(),
candidates,
string_candidates: Arc::new(Vec::new()),
hovered_index: None,
filtered_entries: vec![
ProfilePickerEntry::Profile(ProfileMatchEntry {
candidate_index: 0,

View File

@@ -1,8 +1,8 @@
use crate::{
language_model_selector::{LanguageModelSelector, language_model_selector},
ui::BurnModeTooltip,
ui::{BurnModeTooltip, ModelSelectorTooltip},
};
use agent_settings::{AgentSettings, CompletionMode};
use agent_settings::CompletionMode;
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
@@ -33,7 +33,8 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
Role,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, popover_menu::PickerPopoverMenu};
@@ -71,7 +72,7 @@ use workspace::{
pane,
searchable::{SearchEvent, SearchableItem},
};
use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector};
use zed_actions::agent::{AddSelectionToThread, PasteRaw, ToggleModelSelector};
use crate::CycleFavoriteModels;
@@ -1698,6 +1699,9 @@ impl TextThreadEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let editor_clipboard_selections = cx
.read_from_clipboard()
.and_then(|item| item.entries().first().cloned())
@@ -1708,84 +1712,101 @@ impl TextThreadEditor {
_ => None,
});
let has_file_context = editor_clipboard_selections
.as_ref()
.is_some_and(|selections| {
selections
.iter()
.any(|sel| sel.file_path.is_some() && sel.line_range.is_some())
});
// Insert creases for pasted clipboard selections that:
// 1. Contain exactly one selection
// 2. Have an associated file path
// 3. Span multiple lines (not single-line selections)
// 4. Belong to a file that exists in the current project
let should_insert_creases = util::maybe!({
let selections = editor_clipboard_selections.as_ref()?;
if selections.len() > 1 {
return Some(false);
}
let selection = selections.first()?;
let file_path = selection.file_path.as_ref()?;
let line_range = selection.line_range.as_ref()?;
if has_file_context {
if let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) =
clipboard_item.entries().first()
{
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
if line_range.start() == line_range.end() {
return Some(false);
}
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
Some(
workspace
.read(cx)
.project()
.read(cx)
.project_path_for_absolute_path(file_path, cx)
.is_some(),
)
})
.unwrap_or(false);
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
if let Some(selections) = editor_clipboard_selections {
cx.stop_propagation();
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
let text = clipboard_text.text();
self.editor.update(cx, |editor, cx| {
let mut current_offset = 0;
let weak_editor = cx.entity().downgrade();
editor.insert(&formatted_text, window, cx);
for selection in selections {
if let (Some(file_path), Some(line_range)) =
(selection.file_path, selection.line_range)
{
let selected_text =
&text[current_offset..current_offset + selection.len];
let fence = assistant_slash_commands::codeblock_fence_for_path(
file_path.to_str(),
Some(line_range.clone()),
);
let formatted_text = format!("{fence}{selected_text}\n```");
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let insert_point = editor
.selections
.newest::<Point>(&editor.display_snapshot(cx))
.head();
let start_row = MultiBufferRow(insert_point.row);
editor.insert("\n", window, cx);
editor.insert(&formatted_text, window, cx);
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_before = snapshot.anchor_after(insert_point);
let anchor_after = editor
.selections
.newest_anchor()
.head()
.bias_left(&snapshot);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
editor.insert("\n", window, cx);
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
}
let crease_text = acp_thread::selection_name(
Some(file_path.as_ref()),
&line_range,
);
let fold_placeholder = quote_selection_fold_placeholder(
crease_text,
weak_editor.clone(),
);
let crease = Crease::inline(
anchor_before..anchor_after,
fold_placeholder,
render_quote_selection_output_toggle,
|_, _, _, _| Empty.into_any(),
);
editor.insert_creases(vec![crease], cx);
editor.fold_at(start_row, window, cx);
current_offset += selection.len;
if !selection.is_entire_line && current_offset < text.len() {
current_offset += 1;
}
}
});
return;
}
}
});
return;
}
}
}
@@ -1944,6 +1965,12 @@ impl TextThreadEditor {
}
}
fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
editor.paste(&editor::actions::Paste, window, cx);
});
}
fn update_image_blocks(&mut self, cx: &mut Context<Self>) {
self.editor.update(cx, |editor, cx| {
let buffer = editor.buffer().read(cx).snapshot(cx);
@@ -2205,10 +2232,10 @@ impl TextThreadEditor {
.default_model()
.map(|default| default.provider);
let provider_icon = match active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
let provider_icon = active_provider
.as_ref()
.map(|p| p.icon())
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
let focus_handle = self.editor().focus_handle(cx);
@@ -2218,43 +2245,25 @@ impl TextThreadEditor {
(Color::Muted, IconName::ChevronDown)
};
let tooltip = Tooltip::element({
move |_, cx| {
let focus_handle = focus_handle.clone();
let should_show_cycle_row = !AgentSettings::get_global(cx)
.favorite_model_ids()
.is_empty();
let provider_icon_element = match provider_icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall);
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Change Model"))
.child(KeyBinding::for_action_in(
&ToggleModelSelector,
&focus_handle,
cx,
)),
)
.when(should_show_cycle_row, |this| {
this.child(
h_flex()
.pt_1()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(Label::new("Cycle Favorited Models"))
.child(KeyBinding::for_action_in(
&CycleFavoriteModels,
&focus_handle,
cx,
)),
)
})
.into_any()
let show_cycle_row = self
.language_model_selector
.read(cx)
.delegate
.favorites_count()
> 1;
let tooltip = Tooltip::element({
move |_, _cx| {
ModelSelectorTooltip::new(focus_handle.clone())
.show_cycle_row(show_cycle_row)
.into_any_element()
}
});
@@ -2265,7 +2274,7 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
.child(provider_icon_element)
.child(
Label::new(model_name)
.color(color)
@@ -2627,6 +2636,7 @@ impl Render for TextThreadEditor {
.capture_action(cx.listener(TextThreadEditor::copy))
.capture_action(cx.listener(TextThreadEditor::cut))
.capture_action(cx.listener(TextThreadEditor::paste))
.on_action(cx.listener(TextThreadEditor::paste_raw))
.capture_action(cx.listener(TextThreadEditor::cycle_message_role))
.capture_action(cx.listener(TextThreadEditor::confirm_command))
.on_action(cx.listener(TextThreadEditor::assist))

View File

@@ -4,6 +4,7 @@ mod burn_mode_tooltip;
mod claude_code_onboarding_modal;
mod end_trial_upsell;
mod hold_for_default;
mod mention_crease;
mod model_selector_components;
mod onboarding_modal;
mod usage_callout;
@@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*;
pub use claude_code_onboarding_modal::*;
pub use end_trial_upsell::*;
pub use hold_for_default::*;
pub use mention_crease::*;
pub use model_selector_components::*;
pub use onboarding_modal::*;
pub use usage_callout::*;

View File

@@ -27,7 +27,7 @@ impl RenderOnce for HoldForDefault {
PlatformStyle::platform(),
None,
Some(TextSize::Default.rems(cx).into()),
true,
false,
)))
.child(div().map(|this| {
if self.is_default {

View File

@@ -0,0 +1,100 @@
use std::time::Duration;
use gpui::{Animation, AnimationExt, AnyView, IntoElement, Window, pulsating_between};
use settings::Settings;
use theme::ThemeSettings;
use ui::{ButtonLike, TintColor, prelude::*};
#[derive(IntoElement)]
pub struct MentionCrease {
id: ElementId,
icon: SharedString,
label: SharedString,
is_toggled: bool,
is_loading: bool,
image_preview: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
}
impl MentionCrease {
pub fn new(
id: impl Into<ElementId>,
icon: impl Into<SharedString>,
label: impl Into<SharedString>,
) -> Self {
Self {
id: id.into(),
icon: icon.into(),
label: label.into(),
is_toggled: false,
is_loading: false,
image_preview: None,
}
}
pub fn is_toggled(mut self, is_toggled: bool) -> Self {
self.is_toggled = is_toggled;
self
}
pub fn is_loading(mut self, is_loading: bool) -> Self {
self.is_loading = is_loading;
self
}
pub fn image_preview(
mut self,
builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
self.image_preview = Some(Box::new(builder));
self
}
}
impl RenderOnce for MentionCrease {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let font_size = settings.agent_buffer_font_size(cx);
let buffer_font = settings.buffer_font.clone();
let button_height = DefiniteLength::Absolute(AbsoluteLength::Pixels(
px(window.line_height().into()) - px(1.),
));
ButtonLike::new(self.id)
.style(ButtonStyle::Outlined)
.size(ButtonSize::Compact)
.height(button_height)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.toggle_state(self.is_toggled)
.when_some(self.image_preview, |this, image_preview| {
this.hoverable_tooltip(image_preview)
})
.child(
h_flex()
.pb_px()
.gap_1()
.font(buffer_font)
.text_size(font_size)
.child(
Icon::from_path(self.icon.clone())
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(self.label.clone())
.map(|this| {
if self.is_loading {
this.with_animation(
"loading-context-crease",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.opacity(delta),
)
.into_any()
} else {
this.into_any()
}
}),
)
}
}

View File

@@ -1,5 +1,13 @@
use gpui::{Action, FocusHandle, prelude::*};
use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
use zed_actions::agent::ToggleModelSelector;
use crate::CycleFavoriteModels;
enum ModelIcon {
Name(IconName),
Path(SharedString),
}
#[derive(IntoElement)]
pub struct ModelSelectorHeader {
@@ -39,11 +47,11 @@ impl RenderOnce for ModelSelectorHeader {
pub struct ModelSelectorListItem {
index: usize,
title: SharedString,
icon: Option<IconName>,
icon: Option<ModelIcon>,
is_selected: bool,
is_focused: bool,
is_favorite: bool,
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl ModelSelectorListItem {
@@ -60,7 +68,12 @@ impl ModelSelectorListItem {
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self.icon = Some(ModelIcon::Name(icon));
self
}
pub fn icon_path(mut self, path: SharedString) -> Self {
self.icon = Some(ModelIcon::Path(path));
self
}
@@ -79,7 +92,10 @@ impl ModelSelectorListItem {
self
}
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
pub fn on_toggle_favorite(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_toggle_favorite = Some(Box::new(handler));
self
}
@@ -105,9 +121,12 @@ impl RenderOnce for ModelSelectorListItem {
.gap_1p5()
.when_some(self.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small),
match icon {
ModelIcon::Name(icon_name) => Icon::new(icon_name),
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
}
.color(model_icon_color)
.size(IconSize::Small),
)
})
.child(Label::new(self.title).truncate()),
@@ -128,7 +147,7 @@ impl RenderOnce for ModelSelectorListItem {
.icon_color(color)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text(tooltip))
.on_click(move |_, _, cx| (handle_click)(cx)),
.on_click(move |event, window, cx| (handle_click)(event, window, cx)),
)
}
}))
@@ -174,3 +193,57 @@ impl RenderOnce for ModelSelectorFooter {
)
}
}
#[derive(IntoElement)]
pub struct ModelSelectorTooltip {
focus_handle: FocusHandle,
show_cycle_row: bool,
}
impl ModelSelectorTooltip {
pub fn new(focus_handle: FocusHandle) -> Self {
Self {
focus_handle,
show_cycle_row: true,
}
}
pub fn show_cycle_row(mut self, show: bool) -> Self {
self.show_cycle_row = show;
self
}
}
impl RenderOnce for ModelSelectorTooltip {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.gap_1()
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Change Model"))
.child(KeyBinding::for_action_in(
&ToggleModelSelector,
&self.focus_handle,
cx,
)),
)
.when(self.show_cycle_row, |this| {
this.child(
h_flex()
.pt_1()
.gap_2()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.justify_between()
.child(Label::new("Cycle Favorited Models"))
.child(KeyBinding::for_action_in(
&CycleFavoriteModels,
&self.focus_handle,
cx,
)),
)
})
}
}

View File

@@ -1,5 +1,5 @@
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -411,7 +411,22 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -432,11 +447,14 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(thread_timestamp)
Label::new(display_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -1,9 +1,9 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, ListBulletItem, prelude::*};
pub struct ApiKeysWithProviders {
configured_providers: Vec<(IconName, SharedString)>,
configured_providers: Vec<(IconOrSvg, SharedString)>,
}
impl ApiKeysWithProviders {
@@ -13,7 +13,8 @@ impl ApiKeysWithProviders {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.configured_providers = Self::compute_configured_providers(cx)
}
_ => {}
@@ -26,9 +27,9 @@ impl ApiKeysWithProviders {
}
}
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
@@ -47,7 +48,14 @@ impl Render for ApiKeysWithProviders {
.map(|(icon, name)| {
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(
match icon {
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
}
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(name))
});
div()

View File

@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
configured_providers: Vec<(IconName, SharedString)>,
has_configured_providers: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
}
@@ -27,8 +27,9 @@ impl AgentPanelOnboarding {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_available_providers(cx)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.has_configured_providers = Self::has_configured_providers(cx)
}
_ => {}
},
@@ -38,20 +39,16 @@ impl AgentPanelOnboarding {
Self {
user_store,
client,
configured_providers: Self::compute_available_providers(cx),
has_configured_providers: Self::has_configured_providers(cx),
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
}
}
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
fn has_configured_providers(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0))
.collect()
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
}
}
@@ -81,7 +78,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
this
} else {
this.child(ApiKeysWithoutProviders::new())

View File

@@ -15,7 +15,6 @@ path = "src/buffer_diff.rs"
test-support = ["settings"]
[dependencies]
anyhow.workspace = true
clock.workspace = true
futures.workspace = true
git2.workspace = true

File diff suppressed because it is too large Load Diff

View File

@@ -113,7 +113,7 @@ impl CopilotSweAgentBot {
const USER_ID: i32 = 198982749;
/// The alias of the GitHub copilot user. Although https://api.github.com/users/copilot
/// yields a 404, GitHub still refers to the copilot bot user as @Copilot in some cases.
const NAME_ALIAS: &'static str = "copilot";
const NAME_ALIAS: &'static str = "Copilot";
/// Returns the `created_at` timestamp for the Dependabot bot user.
fn created_at() -> &'static NaiveDateTime {

View File

@@ -2647,13 +2647,13 @@ async fn test_git_diff_base_change(
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
let buffer = buffer_local_a.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
);
});
@@ -2677,13 +2677,13 @@ async fn test_git_diff_base_change(
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
let buffer = remote_buffer_a.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
);
});
@@ -2699,13 +2699,13 @@ async fn test_git_diff_base_change(
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
let buffer = remote_buffer_a.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(committed_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(
1..2,
"TWO\n",
@@ -2731,13 +2731,13 @@ async fn test_git_diff_base_change(
local_unstaged_diff_a.read_with(cx_a, |diff, cx| {
let buffer = buffer_local_a.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(new_staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
);
});
@@ -2746,13 +2746,13 @@ async fn test_git_diff_base_change(
remote_unstaged_diff_a.read_with(cx_b, |diff, cx| {
let buffer = remote_buffer_a.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(new_staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
);
});
@@ -2760,13 +2760,13 @@ async fn test_git_diff_base_change(
remote_uncommitted_diff_a.read_with(cx_b, |diff, cx| {
let buffer = remote_buffer_a.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(new_committed_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(
1..2,
"TWO_HUNDRED\n",
@@ -2813,13 +2813,13 @@ async fn test_git_diff_base_change(
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
let buffer = buffer_local_b.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&diff.base_text_string().unwrap(),
&diff.base_text_string(cx).unwrap(),
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
);
});
@@ -2842,11 +2842,11 @@ async fn test_git_diff_base_change(
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
let buffer = remote_buffer_b.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&staged_text,
&[(1..2, "", "two\n", DiffHunkStatus::added_none())],
@@ -2864,11 +2864,11 @@ async fn test_git_diff_base_change(
local_unstaged_diff_b.read_with(cx_a, |diff, cx| {
let buffer = buffer_local_b.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(new_staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&new_staged_text,
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
@@ -2878,11 +2878,11 @@ async fn test_git_diff_base_change(
remote_unstaged_diff_b.read_with(cx_b, |diff, cx| {
let buffer = remote_buffer_b.read(cx);
assert_eq!(
diff.base_text_string().as_deref(),
diff.base_text_string(cx).as_deref(),
Some(new_staged_text.as_str())
);
assert_hunks(
diff.hunks_in_row_range(0..4, buffer, cx),
diff.snapshot(cx).hunks_in_row_range(0..4, buffer),
buffer,
&new_staged_text,
&[(2..3, "", "three\n", DiffHunkStatus::added_none())],
@@ -5195,7 +5195,7 @@ async fn test_project_search(
cx,
)
});
while let Ok(result) = search_rx.recv().await {
while let Ok(result) = search_rx.rx.recv().await {
match result {
SearchResult::Buffer { buffer, ranges } => {
results.entry(buffer).or_insert(ranges);
@@ -6745,8 +6745,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
});
// Split pane to the right
pane.update(cx, |pane, cx| {
pane.split(workspace::SplitDirection::Right, cx);
pane.update_in(cx, |pane, window, cx| {
pane.split(
workspace::SplitDirection::Right,
workspace::SplitMode::default(),
window,
cx,
);
});
cx.run_until_parked();
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());

View File

@@ -905,7 +905,7 @@ impl RandomizedTest for ProjectCollaborationTest {
drop(project);
let search = cx.executor().spawn(async move {
let mut results = HashMap::default();
while let Ok(result) = search.recv().await {
while let Ok(result) = search.rx.recv().await {
if let SearchResult::Buffer { buffer, ranges } = result {
results.entry(buffer).or_insert(ranges);
}
@@ -1377,7 +1377,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.get_unstaged_diff(host_buffer.read(cx).remote_id(), cx)
.unwrap()
.read(cx)
.base_text_string()
.base_text_string(cx)
});
let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
project
@@ -1386,7 +1386,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.get_unstaged_diff(guest_buffer.read(cx).remote_id(), cx)
.unwrap()
.read(cx)
.base_text_string()
.base_text_string(cx)
});
assert_eq!(
guest_diff_base, host_diff_base,

View File

@@ -855,8 +855,6 @@ async fn test_slow_adapter_startup_retries(
#[gpui::test]
async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &mut TestAppContext) {
use project::trusted_worktrees::RemoteHostLocation;
cx_a.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
project::trusted_worktrees::init(HashMap::default(), None, None, cx);
@@ -991,23 +989,19 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
});
assert_eq!(worktree_ids.len(), 2);
let remote_host = project_a.read_with(cx_a, |project, cx| {
project
.remote_connection_options(cx)
.map(RemoteHostLocation::from)
});
let trusted_worktrees =
cx_a.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
let can_trust_a =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
let can_trust_b =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[0], cx)
});
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[1], cx)
});
assert!(!can_trust_a, "project_a should be restricted initially");
assert!(!can_trust_b, "project_b should be restricted initially");
let worktree_store = project_a.read_with(cx_a, |project, _| project.worktree_store());
let has_restricted = trusted_worktrees.read_with(cx_a, |store, cx| {
store.has_restricted_worktrees(&worktree_store, cx)
});
@@ -1054,8 +1048,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
trusted_worktrees.update(cx_a, |store, cx| {
store.trust(
&worktree_store,
HashSet::from_iter([PathTrust::Worktree(worktree_ids[0])]),
remote_host.clone(),
cx,
);
});
@@ -1080,25 +1074,29 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
"inlay hints should be queried after trust approval"
);
let can_trust_a =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
let can_trust_b =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[0], cx)
});
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[1], cx)
});
assert!(can_trust_a, "project_a should be trusted after trust()");
assert!(!can_trust_b, "project_b should still be restricted");
trusted_worktrees.update(cx_a, |store, cx| {
store.trust(
&worktree_store,
HashSet::from_iter([PathTrust::Worktree(worktree_ids[1])]),
remote_host.clone(),
cx,
);
});
let can_trust_a =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[0], cx));
let can_trust_b =
trusted_worktrees.update(cx_a, |store, cx| store.can_trust(worktree_ids[1], cx));
let can_trust_a = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[0], cx)
});
let can_trust_b = trusted_worktrees.update(cx_a, |store, cx| {
store.can_trust(&worktree_store, worktree_ids[1], cx)
});
assert!(can_trust_a, "project_a should remain trusted");
assert!(can_trust_b, "project_b should now be trusted");

View File

@@ -31,9 +31,9 @@ use smallvec::SmallVec;
use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tab, Tooltip,
prelude::*, tooltip_container,
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, CopyButton, Facepile,
HighlightedLabel, Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem,
Tab, Tooltip, prelude::*, tooltip_container,
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
@@ -2527,16 +2527,9 @@ impl CollabPanel {
let button = match section {
Section::ActiveCall => channel_link.map(|channel_link| {
let channel_link_copy = channel_link;
IconButton::new("channel-link", IconName::Copy)
.icon_size(IconSize::Small)
.size(ButtonSize::None)
CopyButton::new(channel_link)
.visible_on_hover("section-header")
.on_click(move |_, _, cx| {
let item = ClipboardItem::new_string(channel_link_copy.clone());
cx.write_to_clipboard(item)
})
.tooltip(Tooltip::text("Copy channel link"))
.tooltip_label("Copy Channel Link")
.into_any_element()
}),
Section::Contacts => Some(

View File

@@ -0,0 +1,45 @@
[package]
name = "component_preview"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/component_preview.rs"
[features]
default = []
preview = []
test-support = ["db/test-support"]
[dependencies]
anyhow.workspace = true
client.workspace = true
collections.workspace = true
component.workspace = true
db.workspace = true
fs.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
node_runtime.workspace = true
notifications.workspace = true
project.workspace = true
release_channel.workspace = true
reqwest_client.workspace = true
session.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
ui_input.workspace = true
uuid.workspace = true
workspace.workspace = true
[[example]]
name = "component_preview"
path = "examples/component_preview.rs"
required-features = ["preview"]

View File

@@ -0,0 +1 @@
LICENSE-GPL

View File

@@ -0,0 +1,18 @@
//! Component Preview Example
//!
//! Run with: `cargo run -p component_preview --example component_preview --features="preview"`
//!
//! To use this in other projects, add the following to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! component_preview = { path = "../component_preview", features = ["preview"] }
//!
//! [[example]]
//! name = "component_preview"
//! path = "examples/component_preview.rs"
//! ```
fn main() {
component_preview::run_component_preview();
}

View File

@@ -1,7 +1,4 @@
//! # Component Preview
//!
//! A view for exploring Zed components.
mod component_preview_example;
mod persistence;
use client::UserStore;
@@ -11,18 +8,21 @@ use gpui::{
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
};
use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle};
use languages::LanguageRegistry;
use language::LanguageRegistry;
use notifications::status_toast::{StatusToast, ToastIcon};
use persistence::COMPONENT_PREVIEW_DB;
use project::Project;
use std::{iter::Iterator, ops::Range, sync::Arc};
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
use ui_input::InputField;
use workspace::AppState;
use workspace::{
AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
item::ItemEvent,
Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, item::ItemEvent,
};
#[allow(unused_imports)]
pub use component_preview_example::*;
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
workspace::register_serializable_item::<ComponentPreview>(cx);

View File

@@ -0,0 +1,145 @@
/// Run the component preview application.
///
/// This initializes the application with minimal required infrastructure
/// and opens a workspace with the ComponentPreview item.
#[cfg(feature = "preview")]
pub fn run_component_preview() {
use fs::RealFs;
use gpui::{
AppContext as _, Application, Bounds, KeyBinding, WindowBounds, WindowOptions, actions,
size,
};
use client::{Client, UserStore};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use project::Project;
use reqwest_client::ReqwestClient;
use session::{AppSession, Session};
use std::sync::Arc;
use ui::{App, px};
use workspace::{AppState, Workspace, WorkspaceStore};
use crate::{ComponentPreview, init};
actions!(zed, [Quit]);
fn quit(_: &Quit, cx: &mut App) {
cx.quit();
}
Application::new().run(|cx| {
component::init();
cx.on_action(quit);
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
let version = release_channel::AppVersion::load(env!("CARGO_PKG_VERSION"), None, None);
release_channel::init(version, cx);
let http_client =
ReqwestClient::user_agent("component_preview").expect("Failed to create HTTP client");
cx.set_http_client(Arc::new(http_client));
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
<dyn fs::Fs>::set_global(fs.clone(), cx);
settings::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
let client = Client::production(cx);
client::init(&client, cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let session_id = uuid::Uuid::new_v4().to_string();
let session = cx.background_executor().block(Session::new(session_id));
let session = cx.new(|cx| AppSession::new(session, cx));
let node_runtime = NodeRuntime::unavailable();
let app_state = Arc::new(AppState {
languages,
client,
user_store,
workspace_store,
fs,
build_window_options: |_, _| Default::default(),
node_runtime,
session,
});
AppState::set_global(Arc::downgrade(&app_state), cx);
workspace::init(app_state.clone(), cx);
init(app_state.clone(), cx);
let size = size(px(1200.), px(800.));
let bounds = Bounds::centered(None, size, cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
{
move |window, cx| {
let app_state = app_state;
theme::setup_ui_font(window, cx);
let project = Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
false,
cx,
);
let workspace = cx.new(|cx| {
Workspace::new(
Default::default(),
project.clone(),
app_state.clone(),
window,
cx,
)
});
workspace.update(cx, |workspace, cx| {
let weak_workspace = cx.entity().downgrade();
let language_registry = app_state.languages.clone();
let user_store = app_state.user_store.clone();
let component_preview = cx.new(|cx| {
ComponentPreview::new(
weak_workspace,
project,
language_registry,
user_store,
None,
None,
window,
cx,
)
.expect("Failed to create component preview")
});
workspace.add_item_to_active_pane(
Box::new(component_preview),
None,
true,
window,
cx,
);
});
workspace
}
},
)
.expect("Failed to open component preview window");
cx.activate(true);
});
}

View File

@@ -34,7 +34,7 @@ impl StdioTransport {
let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?;
let builder = ShellBuilder::new(&shell, cfg!(windows)).non_interactive();
let mut command =
builder.build_command(Some(binary.executable.display().to_string()), &binary.args);
builder.build_smol_command(Some(binary.executable.display().to_string()), &binary.args);
command
.envs(binary.env.unwrap_or_default())

View File

@@ -24,7 +24,7 @@ use std::{
time::Duration,
};
use task::TcpArgumentsTemplate;
use util::ConnectionResult;
use util::{ConnectionResult, ResultExt, process::Child};
use crate::{
adapters::{DebugAdapterBinary, TcpArguments},
@@ -528,7 +528,7 @@ impl TcpTransport {
command.args(&binary.arguments);
command.envs(&binary.envs);
let mut p = Child::spawn(command, Stdio::null())
let mut p = Child::spawn(command, Stdio::null(), Stdio::piped(), Stdio::piped())
.with_context(|| "failed to start debug adapter.")?;
stdout_task = p.stdout.take().map(|stdout| {
@@ -582,7 +582,7 @@ impl Transport for TcpTransport {
fn kill(&mut self) {
if let Some(process) = &mut *self.process.lock() {
process.kill();
process.kill().log_err();
}
}
@@ -647,7 +647,7 @@ impl Transport for TcpTransport {
impl Drop for TcpTransport {
fn drop(&mut self) {
if let Some(mut p) = self.process.lock().take() {
p.kill()
p.kill().log_err();
}
}
}
@@ -678,7 +678,7 @@ impl StdioTransport {
command.args(&binary.arguments);
command.envs(&binary.envs);
let mut process = Child::spawn(command, Stdio::piped())?;
let mut process = Child::spawn(command, Stdio::piped(), Stdio::piped(), Stdio::piped())?;
let _stderr_task = process.stderr.take().map(|stderr| {
cx.background_spawn(TransportDelegate::handle_adapter_log(
@@ -703,7 +703,7 @@ impl Transport for StdioTransport {
}
fn kill(&mut self) {
self.process.lock().kill();
self.process.lock().kill().log_err();
}
fn connect(
@@ -731,7 +731,7 @@ impl Transport for StdioTransport {
impl Drop for StdioTransport {
fn drop(&mut self) {
self.process.lock().kill();
self.process.lock().kill().log_err();
}
}
@@ -1024,68 +1024,3 @@ impl Transport for FakeTransport {
self
}
}
struct Child {
process: smol::process::Child,
}
impl std::ops::Deref for Child {
type Target = smol::process::Child;
fn deref(&self) -> &Self::Target {
&self.process
}
}
impl std::ops::DerefMut for Child {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.process
}
}
impl Child {
fn into_inner(self) -> smol::process::Child {
self.process
}
#[cfg(not(windows))]
fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
util::set_pre_exec_to_start_new_session(&mut command);
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
Ok(Self { process })
}
#[cfg(windows)]
fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
// TODO(windows): create a job object and add the child process handle to it,
// see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
let mut command = smol::process::Command::from(command);
let process = command
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("failed to spawn command `{command:?}`",))?;
Ok(Self { process })
}
#[cfg(not(windows))]
fn kill(&mut self) {
let pid = self.process.id();
unsafe {
libc::killpg(pid as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
fn kill(&mut self) {
// TODO(windows): terminate the job object in kill
let _ = self.process.kill();
}
}

View File

@@ -1579,8 +1579,10 @@ impl Panel for DebugPanel {
Some(proto::PanelId::DebugPanel)
}
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
Some(IconName::Debug)
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
DebuggerSettings::get_global(cx)
.button
.then_some(IconName::Debug)
}
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {

View File

@@ -103,8 +103,9 @@ impl Model {
pub fn max_output_tokens(&self) -> Option<u64> {
match self {
Self::Chat => Some(8_192),
Self::Reasoner => Some(64_000),
// Their API treats this max against the context window, which means we hit the limit a lot
// Using the default value of None in the API instead
Self::Chat | Self::Reasoner => None,
Self::Custom {
max_output_tokens, ..
} => *max_output_tokens,

View File

@@ -7,8 +7,6 @@ license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
command_palette.workspace = true
gpui.workspace = true
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
# Ask @maxdeviant about this before bumping.
mdbook = "= 0.4.40"
@@ -17,7 +15,6 @@ serde.workspace = true
serde_json.workspace = true
settings.workspace = true
util.workspace = true
zed.workspace = true
zlog.workspace = true
task.workspace = true
theme.workspace = true
@@ -27,4 +24,4 @@ workspace = true
[[bin]]
name = "docs_preprocessor"
path = "src/main.rs"
path = "src/main.rs"

View File

@@ -22,16 +22,13 @@ static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
});
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(load_all_actions);
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
fn main() -> Result<()> {
zlog::init();
zlog::init_output_stderr();
// call a zed:: function so everything in `zed` crate is linked and
// all actions in the actual app are registered
zed::stdout_is_a_pty();
let args = std::env::args().skip(1).collect::<Vec<_>>();
match args.get(0).map(String::as_str) {
@@ -72,8 +69,8 @@ enum PreprocessorError {
impl PreprocessorError {
fn new_for_not_found_action(action_name: String) -> Self {
for action in &*ALL_ACTIONS {
for alias in action.deprecated_aliases {
if alias == &action_name {
for alias in &action.deprecated_aliases {
if alias == action_name.as_str() {
return PreprocessorError::DeprecatedActionUsed {
used: action_name,
should_be: action.name.to_string(),
@@ -214,7 +211,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Prepr
chapter.content = regex
.replace_all(&chapter.content, |caps: &regex::Captures| {
let action = caps[1].trim();
if find_action_by_name(action).is_none() {
if is_missing_action(action) {
errors.insert(PreprocessorError::new_for_not_found_action(
action.to_string(),
));
@@ -244,10 +241,12 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
.replace_all(&chapter.content, |caps: &regex::Captures| {
let name = caps[1].trim();
let Some(action) = find_action_by_name(name) else {
errors.insert(PreprocessorError::new_for_not_found_action(
name.to_string(),
));
return String::new();
if actions_available() {
errors.insert(PreprocessorError::new_for_not_found_action(
name.to_string(),
));
}
return format!("<code class=\"hljs\">{}</code>", name);
};
format!("<code class=\"hljs\">{}</code>", &action.human_name)
})
@@ -257,11 +256,19 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Preproces
fn find_action_by_name(name: &str) -> Option<&ActionDef> {
ALL_ACTIONS
.binary_search_by(|action| action.name.cmp(name))
.binary_search_by(|action| action.name.as_str().cmp(name))
.ok()
.map(|index| &ALL_ACTIONS[index])
}
fn actions_available() -> bool {
!ALL_ACTIONS.is_empty()
}
fn is_missing_action(name: &str) -> bool {
actions_available() && find_action_by_name(name).is_none()
}
fn find_binding(os: &str, action: &str) -> Option<String> {
let keymap = match os {
"macos" => &KEYMAP_MACOS,
@@ -384,18 +391,13 @@ fn template_and_validate_json_snippets(book: &mut Book, errors: &mut HashSet<Pre
let keymap = settings::KeymapFile::parse(&snippet_json_fixed)
.context("Failed to parse keymap JSON")?;
for section in keymap.sections() {
for (keystrokes, action) in section.bindings() {
keystrokes
.split_whitespace()
.map(|source| gpui::Keystroke::parse(source))
.collect::<std::result::Result<Vec<_>, _>>()
.context("Failed to parse keystroke")?;
for (_keystrokes, action) in section.bindings() {
if let Some((action_name, _)) = settings::KeymapFile::parse_action(action)
.map_err(|err| anyhow::format_err!(err))
.context("Failed to parse action")?
{
anyhow::ensure!(
find_action_by_name(action_name).is_some(),
!is_missing_action(action_name),
"Action not found: {}",
action_name
);
@@ -491,27 +493,35 @@ where
});
}
#[derive(Debug, serde::Serialize)]
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct ActionDef {
name: &'static str,
name: String,
human_name: String,
deprecated_aliases: &'static [&'static str],
docs: Option<&'static str>,
deprecated_aliases: Vec<String>,
#[serde(rename = "documentation")]
docs: Option<String>,
}
fn dump_all_gpui_actions() -> Vec<ActionDef> {
let mut actions = gpui::generate_list_of_all_registered_actions()
.map(|action| ActionDef {
name: action.name,
human_name: command_palette::humanize_action_name(action.name),
deprecated_aliases: action.deprecated_aliases,
docs: action.documentation,
})
.collect::<Vec<ActionDef>>();
actions.sort_by_key(|a| a.name);
actions
fn load_all_actions() -> Vec<ActionDef> {
let asset_path = concat!(env!("CARGO_MANIFEST_DIR"), "/actions.json");
match std::fs::read_to_string(asset_path) {
Ok(content) => {
let mut actions: Vec<ActionDef> =
serde_json::from_str(&content).expect("Failed to parse actions.json");
actions.sort_by(|a, b| a.name.cmp(&b.name));
actions
}
Err(err) => {
if std::env::var("CI").is_ok() {
panic!("actions.json not found at {}: {}", asset_path, err);
}
eprintln!(
"Warning: actions.json not found, action validation will be skipped: {}",
err
);
Vec::new()
}
}
}
fn handle_postprocessing() -> Result<()> {
@@ -647,7 +657,7 @@ fn generate_big_table_of_actions() -> String {
let mut output = String::new();
let mut actions_sorted = actions.iter().collect::<Vec<_>>();
actions_sorted.sort_by_key(|a| a.name);
actions_sorted.sort_by_key(|a| a.name.as_str());
// Start the definition list with custom styling for better spacing
output.push_str("<dl style=\"line-height: 1.8;\">\n");
@@ -664,7 +674,7 @@ fn generate_big_table_of_actions() -> String {
output.push_str("<dd style=\"margin-left: 2em; margin-bottom: 1em;\">\n");
// Add the description, escaping HTML if needed
if let Some(description) = action.docs {
if let Some(description) = action.docs.as_ref() {
output.push_str(
&description
.replace("&", "&amp;")
@@ -674,7 +684,7 @@ fn generate_big_table_of_actions() -> String {
output.push_str("<br>\n");
}
output.push_str("Keymap Name: <code>");
output.push_str(action.name);
output.push_str(&action.name);
output.push_str("</code><br>\n");
if !action.deprecated_aliases.is_empty() {
output.push_str("Deprecated Alias(es): ");

View File

@@ -19,6 +19,7 @@ ai_onboarding.workspace = true
anyhow.workspace = true
arrayvec.workspace = true
brotli.workspace = true
buffer_diff.workspace = true
client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
@@ -52,7 +53,9 @@ settings.workspace = true
strum.workspace = true
telemetry.workspace = true
telemetry_events.workspace = true
text.workspace = true
thiserror.workspace = true
time.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true

View File

@@ -0,0 +1,375 @@
use crate::{
EditPredictionStore, StoredEvent,
cursor_excerpt::editable_and_context_ranges_for_cursor_position, example_spec::ExampleSpec,
};
use anyhow::Result;
use buffer_diff::BufferDiffSnapshot;
use collections::HashMap;
use gpui::{App, Entity, Task};
use language::{Buffer, ToPoint as _};
use project::Project;
use std::{collections::hash_map, fmt::Write as _, path::Path, sync::Arc};
use text::{BufferSnapshot as TextBufferSnapshot, ToOffset as _};
pub fn capture_example(
project: Entity<Project>,
buffer: Entity<Buffer>,
cursor_anchor: language::Anchor,
last_event_is_expected_patch: bool,
cx: &mut App,
) -> Option<Task<Result<ExampleSpec>>> {
let ep_store = EditPredictionStore::try_global(cx)?;
let snapshot = buffer.read(cx).snapshot();
let file = snapshot.file()?;
let worktree_id = file.worktree_id(cx);
let repository = project.read(cx).active_repository(cx)?;
let repository_snapshot = repository.read(cx).snapshot();
let worktree = project.read(cx).worktree_for_id(worktree_id, cx)?;
let cursor_path = worktree.read(cx).root_name().join(file.path());
if worktree.read(cx).abs_path() != repository_snapshot.work_directory_abs_path {
return None;
}
let repository_url = repository_snapshot
.remote_origin_url
.clone()
.or_else(|| repository_snapshot.remote_upstream_url.clone())?;
let revision = repository_snapshot.head_commit.as_ref()?.sha.to_string();
let mut events = ep_store.update(cx, |store, cx| {
store.edit_history_for_project_with_pause_split_last_event(&project, cx)
});
let git_store = project.read(cx).git_store().clone();
Some(cx.spawn(async move |mut cx| {
let snapshots_by_path = collect_snapshots(&project, &git_store, &events, &mut cx).await?;
let cursor_excerpt = cx
.background_executor()
.spawn(async move { compute_cursor_excerpt(&snapshot, cursor_anchor) })
.await;
let uncommitted_diff = cx
.background_executor()
.spawn(async move { compute_uncommitted_diff(snapshots_by_path) })
.await;
let mut edit_history = String::new();
let mut expected_patch = String::new();
if last_event_is_expected_patch {
if let Some(stored_event) = events.pop() {
zeta_prompt::write_event(&mut expected_patch, &stored_event.event);
}
}
for stored_event in &events {
zeta_prompt::write_event(&mut edit_history, &stored_event.event);
if !edit_history.ends_with('\n') {
edit_history.push('\n');
}
}
let name = generate_timestamp_name();
Ok(ExampleSpec {
name,
repository_url,
revision,
uncommitted_diff,
cursor_path: cursor_path.as_std_path().into(),
cursor_position: cursor_excerpt,
edit_history,
expected_patch,
})
}))
}
fn compute_cursor_excerpt(
snapshot: &language::BufferSnapshot,
cursor_anchor: language::Anchor,
) -> String {
let cursor_point = cursor_anchor.to_point(snapshot);
let (_editable_range, context_range) =
editable_and_context_ranges_for_cursor_position(cursor_point, snapshot, 100, 50);
let context_start_offset = context_range.start.to_offset(snapshot);
let cursor_offset = cursor_anchor.to_offset(snapshot);
let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);
let mut excerpt = snapshot.text_for_range(context_range).collect::<String>();
if cursor_offset_in_excerpt <= excerpt.len() {
excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER);
}
excerpt
}
async fn collect_snapshots(
project: &Entity<Project>,
git_store: &Entity<project::git_store::GitStore>,
events: &[StoredEvent],
cx: &mut gpui::AsyncApp,
) -> Result<HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>> {
let mut snapshots_by_path = HashMap::default();
for stored_event in events {
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
if let Some((project_path, full_path)) = project.read_with(cx, |project, cx| {
let project_path = project.find_project_path(path, cx)?;
let full_path = project
.worktree_for_id(project_path.worktree_id, cx)?
.read(cx)
.root_name()
.join(&project_path.path)
.as_std_path()
.into();
Some((project_path, full_path))
})? {
if let hash_map::Entry::Vacant(entry) = snapshots_by_path.entry(full_path) {
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})?
.await?;
let diff = git_store
.update(cx, |git_store, cx| {
git_store.open_uncommitted_diff(buffer.clone(), cx)
})?
.await?;
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx))?;
entry.insert((stored_event.old_snapshot.clone(), diff_snapshot));
}
}
}
Ok(snapshots_by_path)
}
fn compute_uncommitted_diff(
snapshots_by_path: HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>,
) -> String {
let mut uncommitted_diff = String::new();
for (full_path, (before_text, diff_snapshot)) in snapshots_by_path {
if let Some(head_text) = &diff_snapshot.base_text_string() {
let file_diff = language::unified_diff(head_text, &before_text.text());
if !file_diff.is_empty() {
let path_str = full_path.to_string_lossy();
writeln!(uncommitted_diff, "--- a/{path_str}").ok();
writeln!(uncommitted_diff, "+++ b/{path_str}").ok();
uncommitted_diff.push_str(&file_diff);
if !uncommitted_diff.ends_with('\n') {
uncommitted_diff.push('\n');
}
}
}
}
uncommitted_diff
}
fn generate_timestamp_name() -> String {
let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
match format {
Ok(format) => {
let now = time::OffsetDateTime::now_local()
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
now.format(&format)
.unwrap_or_else(|_| "unknown-time".to_string())
}
Err(_) => "unknown-time".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use client::{Client, UserStore};
use clock::FakeSystemClock;
use gpui::{AppContext as _, TestAppContext, http_client::FakeHttpClient};
use indoc::indoc;
use language::{Anchor, Point};
use project::{FakeFs, Project};
use serde_json::json;
use settings::SettingsStore;
use std::path::Path;
#[gpui::test]
async fn test_capture_example(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let committed_contents = indoc! {"
fn main() {
one();
two();
three();
four();
five();
six();
seven();
eight();
nine();
}
"};
let disk_contents = indoc! {"
fn main() {
// comment 1
one();
two();
three();
four();
five();
six();
seven();
eight();
// comment 2
nine();
}
"};
fs.insert_tree(
"/project",
json!({
".git": {},
"src": {
"main.rs": disk_contents,
}
}),
)
.await;
fs.set_head_for_repo(
Path::new("/project/.git"),
&[("src/main.rs", committed_contents.to_string())],
"abc123def456",
);
fs.set_remote_for_repo(
Path::new("/project/.git"),
"origin",
"https://github.com/test/repo.git",
);
let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/project/src/main.rs", cx)
})
.await
.unwrap();
let ep_store = cx.read(|cx| EditPredictionStore::try_global(cx).unwrap());
ep_store.update(cx, |ep_store, cx| {
ep_store.register_buffer(&buffer, &project, cx)
});
cx.run_until_parked();
buffer.update(cx, |buffer, cx| {
let point = Point::new(6, 0);
buffer.edit([(point..point, " // comment 3\n")], None, cx);
let point = Point::new(4, 0);
buffer.edit([(point..point, " // comment 4\n")], None, cx);
pretty_assertions::assert_eq!(
buffer.text(),
indoc! {"
fn main() {
// comment 1
one();
two();
// comment 4
three();
four();
// comment 3
five();
six();
seven();
eight();
// comment 2
nine();
}
"}
);
});
cx.run_until_parked();
let mut example = cx
.update(|cx| {
capture_example(project.clone(), buffer.clone(), Anchor::MIN, false, cx).unwrap()
})
.await
.unwrap();
example.name = "test".to_string();
pretty_assertions::assert_eq!(
example,
ExampleSpec {
name: "test".to_string(),
repository_url: "https://github.com/test/repo.git".to_string(),
revision: "abc123def456".to_string(),
uncommitted_diff: indoc! {"
--- a/project/src/main.rs
+++ b/project/src/main.rs
@@ -1,4 +1,5 @@
fn main() {
+ // comment 1
one();
two();
three();
@@ -7,5 +8,6 @@
six();
seven();
eight();
+ // comment 2
nine();
}
"}
.to_string(),
cursor_path: Path::new("project/src/main.rs").into(),
cursor_position: indoc! {"
<|user_cursor|>fn main() {
// comment 1
one();
two();
// comment 4
three();
four();
// comment 3
five();
six();
seven();
eight();
// comment 2
nine();
}
"}
.to_string(),
edit_history: indoc! {"
--- a/project/src/main.rs
+++ b/project/src/main.rs
@@ -2,8 +2,10 @@
// comment 1
one();
two();
+ // comment 4
three();
four();
+ // comment 3
five();
six();
seven();
"}
.to_string(),
expected_patch: "".to_string(),
}
);
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
zlog::init_test();
let http_client = FakeHttpClient::with_404_response();
let client = Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
language_model::init(client.clone(), cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
EditPredictionStore::global(&client, &user_store, cx);
})
}
}

View File

@@ -35,6 +35,7 @@ use semver::Version;
use serde::de::DeserializeOwned;
use settings::{EditPredictionProvider, SettingsStore, update_settings_file};
use std::collections::{VecDeque, hash_map};
use text::Edit;
use workspace::Workspace;
use std::ops::Range;
@@ -57,9 +58,9 @@ pub mod open_ai_response;
mod prediction;
pub mod sweep_ai;
#[cfg(any(test, feature = "test-support", feature = "cli-support"))]
pub mod udiff;
mod capture_example;
mod zed_edit_prediction_delegate;
pub mod zeta1;
pub mod zeta2;
@@ -74,6 +75,7 @@ pub use crate::prediction::EditPrediction;
pub use crate::prediction::EditPredictionId;
use crate::prediction::EditPredictionResult;
pub use crate::sweep_ai::SweepAi;
pub use capture_example::capture_example;
pub use language_model::ApiKeyState;
pub use telemetry_events::EditPredictionRating;
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
@@ -231,8 +233,15 @@ pub struct EditPredictionFinishedDebugEvent {
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
/// An event with associated metadata for reconstructing buffer state.
#[derive(Clone)]
pub struct StoredEvent {
pub event: Arc<zeta_prompt::Event>,
pub old_snapshot: TextBufferSnapshot,
}
struct ProjectState {
events: VecDeque<Arc<zeta_prompt::Event>>,
events: VecDeque<StoredEvent>,
last_event: Option<LastEvent>,
recent_paths: VecDeque<ProjectPath>,
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
@@ -248,7 +257,7 @@ struct ProjectState {
}
impl ProjectState {
pub fn events(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
pub fn events(&self, cx: &App) -> Vec<StoredEvent> {
self.events
.iter()
.cloned()
@@ -260,7 +269,7 @@ impl ProjectState {
.collect()
}
pub fn events_split_by_pause(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
pub fn events_split_by_pause(&self, cx: &App) -> Vec<StoredEvent> {
self.events
.iter()
.cloned()
@@ -415,7 +424,7 @@ impl LastEvent {
&self,
license_detection_watchers: &HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
cx: &App,
) -> Option<Arc<zeta_prompt::Event>> {
) -> Option<StoredEvent> {
let path = buffer_path_with_id_fallback(self.new_file.as_ref(), &self.new_snapshot, cx);
let old_path = buffer_path_with_id_fallback(self.old_file.as_ref(), &self.old_snapshot, cx);
@@ -430,19 +439,22 @@ impl LastEvent {
})
});
let diff = language::unified_diff(&self.old_snapshot.text(), &self.new_snapshot.text());
let diff = compute_diff_between_snapshots(&self.old_snapshot, &self.new_snapshot)?;
if path == old_path && diff.is_empty() {
None
} else {
Some(Arc::new(zeta_prompt::Event::BufferChange {
old_path,
path,
diff,
in_open_source_repo,
// TODO: Actually detect if this edit was predicted or not
predicted: false,
}))
Some(StoredEvent {
event: Arc::new(zeta_prompt::Event::BufferChange {
old_path,
path,
diff,
in_open_source_repo,
// TODO: Actually detect if this edit was predicted or not
predicted: false,
}),
old_snapshot: self.old_snapshot.clone(),
})
}
}
@@ -475,6 +487,52 @@ impl LastEvent {
}
}
pub(crate) fn compute_diff_between_snapshots(
old_snapshot: &TextBufferSnapshot,
new_snapshot: &TextBufferSnapshot,
) -> Option<String> {
let edits: Vec<Edit<usize>> = new_snapshot
.edits_since::<usize>(&old_snapshot.version)
.collect();
let (first_edit, last_edit) = edits.first().zip(edits.last())?;
let old_start_point = old_snapshot.offset_to_point(first_edit.old.start);
let old_end_point = old_snapshot.offset_to_point(last_edit.old.end);
let new_start_point = new_snapshot.offset_to_point(first_edit.new.start);
let new_end_point = new_snapshot.offset_to_point(last_edit.new.end);
const CONTEXT_LINES: u32 = 3;
let old_context_start_row = old_start_point.row.saturating_sub(CONTEXT_LINES);
let new_context_start_row = new_start_point.row.saturating_sub(CONTEXT_LINES);
let old_context_end_row =
(old_end_point.row + 1 + CONTEXT_LINES).min(old_snapshot.max_point().row);
let new_context_end_row =
(new_end_point.row + 1 + CONTEXT_LINES).min(new_snapshot.max_point().row);
let old_start_line_offset = old_snapshot.point_to_offset(Point::new(old_context_start_row, 0));
let new_start_line_offset = new_snapshot.point_to_offset(Point::new(new_context_start_row, 0));
let old_end_line_offset = old_snapshot
.point_to_offset(Point::new(old_context_end_row + 1, 0).min(old_snapshot.max_point()));
let new_end_line_offset = new_snapshot
.point_to_offset(Point::new(new_context_end_row + 1, 0).min(new_snapshot.max_point()));
let old_edit_range = old_start_line_offset..old_end_line_offset;
let new_edit_range = new_start_line_offset..new_end_line_offset;
let old_region_text: String = old_snapshot.text_for_range(old_edit_range).collect();
let new_region_text: String = new_snapshot.text_for_range(new_edit_range).collect();
let diff = language::unified_diff_with_offsets(
&old_region_text,
&new_region_text,
old_context_start_row,
new_context_start_row,
);
Some(diff)
}
fn buffer_path_with_id_fallback(
file: Option<&Arc<dyn File>>,
snapshot: &TextBufferSnapshot,
@@ -643,7 +701,7 @@ impl EditPredictionStore {
&self,
project: &Entity<Project>,
cx: &App,
) -> Vec<Arc<zeta_prompt::Event>> {
) -> Vec<StoredEvent> {
self.projects
.get(&project.entity_id())
.map(|project_state| project_state.events(cx))
@@ -654,7 +712,7 @@ impl EditPredictionStore {
&self,
project: &Entity<Project>,
cx: &App,
) -> Vec<Arc<zeta_prompt::Event>> {
) -> Vec<StoredEvent> {
self.projects
.get(&project.entity_id())
.map(|project_state| project_state.events_split_by_pause(cx))
@@ -1536,8 +1594,10 @@ impl EditPredictionStore {
self.get_or_init_project(&project, cx);
let project_state = self.projects.get(&project.entity_id()).unwrap();
let events = project_state.events(cx);
let has_events = !events.is_empty();
let stored_events = project_state.events(cx);
let has_events = !stored_events.is_empty();
let events: Vec<Arc<zeta_prompt::Event>> =
stored_events.into_iter().map(|e| e.event).collect();
let debug_tx = project_state.debug_tx.clone();
let snapshot = active_buffer.read(cx).snapshot();

View File

@@ -1,5 +1,5 @@
use super::*;
use crate::{udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
use crate::{compute_diff_between_snapshots, udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
use client::{UserStore, test::FakeServer};
use clock::{FakeSystemClock, ReplicaId};
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
@@ -360,7 +360,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
ep_store.edit_history_for_project(&project, cx)
});
assert_eq!(events.len(), 1);
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
assert_eq!(
diff.as_str(),
indoc! {"
@@ -377,7 +377,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
ep_store.edit_history_for_project_with_pause_split_last_event(&project, cx)
});
assert_eq!(events.len(), 2);
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
assert_eq!(
diff.as_str(),
indoc! {"
@@ -389,7 +389,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
"}
);
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].as_ref();
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].event.as_ref();
assert_eq!(
diff.as_str(),
indoc! {"
@@ -2082,6 +2082,74 @@ async fn test_unauthenticated_with_custom_url_allows_prediction_impl(cx: &mut Te
);
}
#[gpui::test]
fn test_compute_diff_between_snapshots(cx: &mut TestAppContext) {
let buffer = cx.new(|cx| {
Buffer::local(
indoc! {"
zero
one
two
three
four
five
six
seven
eight
nine
ten
eleven
twelve
thirteen
fourteen
fifteen
sixteen
seventeen
eighteen
nineteen
twenty
twenty-one
twenty-two
twenty-three
twenty-four
"},
cx,
)
});
let old_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
buffer.update(cx, |buffer, cx| {
let point = Point::new(12, 0);
buffer.edit([(point..point, "SECOND INSERTION\n")], None, cx);
let point = Point::new(8, 0);
buffer.edit([(point..point, "FIRST INSERTION\n")], None, cx);
});
let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
let diff = compute_diff_between_snapshots(&old_snapshot, &new_snapshot).unwrap();
assert_eq!(
diff,
indoc! {"
@@ -6,10 +6,12 @@
five
six
seven
+FIRST INSERTION
eight
nine
ten
eleven
+SECOND INSERTION
twelve
thirteen
fourteen
"}
);
}
#[ctor::ctor]
fn init_logger() {
zlog::init_test();

View File

@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
use std::{fmt::Write as _, mem, path::Path, sync::Arc};
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExampleSpec {
#[serde(default)]
pub name: String,

View File

@@ -6,7 +6,7 @@ use crate::{
use anyhow::{Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{
App, AppContext as _, Entity, SharedString, Task,
App, AppContext as _, Entity, Global, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{OffsetRangeExt as _, ToOffset, ToPoint as _};
@@ -300,14 +300,19 @@ pub const MERCURY_CREDENTIALS_URL: SharedString =
SharedString::new_static("https://api.inceptionlabs.ai/v1/edit/completions");
pub const MERCURY_CREDENTIALS_USERNAME: &str = "mercury-api-token";
pub static MERCURY_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("MERCURY_AI_TOKEN");
pub static MERCURY_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
struct GlobalMercuryApiKey(Entity<ApiKeyState>);
impl Global for GlobalMercuryApiKey {}
pub fn mercury_api_token(cx: &mut App) -> Entity<ApiKeyState> {
MERCURY_API_KEY
.get_or_init(|| {
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()))
})
.clone()
if let Some(global) = cx.try_global::<GlobalMercuryApiKey>() {
return global.0.clone();
}
let entity =
cx.new(|_| ApiKeyState::new(MERCURY_CREDENTIALS_URL, MERCURY_TOKEN_ENV_VAR.clone()));
cx.set_global(GlobalMercuryApiKey(entity.clone()));
entity
}
pub fn load_mercury_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

View File

@@ -1,7 +1,7 @@
use anyhow::Result;
use futures::AsyncReadExt as _;
use gpui::{
App, AppContext as _, Entity, SharedString, Task,
App, AppContext as _, Entity, Global, SharedString, Task,
http_client::{self, AsyncBody, Method},
};
use language::{Point, ToOffset as _};
@@ -272,14 +272,19 @@ pub const SWEEP_CREDENTIALS_URL: SharedString =
SharedString::new_static("https://autocomplete.sweep.dev");
pub const SWEEP_CREDENTIALS_USERNAME: &str = "sweep-api-token";
pub static SWEEP_AI_TOKEN_ENV_VAR: std::sync::LazyLock<EnvVar> = env_var!("SWEEP_AI_TOKEN");
pub static SWEEP_API_KEY: std::sync::OnceLock<Entity<ApiKeyState>> = std::sync::OnceLock::new();
struct GlobalSweepApiKey(Entity<ApiKeyState>);
impl Global for GlobalSweepApiKey {}
pub fn sweep_api_token(cx: &mut App) -> Entity<ApiKeyState> {
SWEEP_API_KEY
.get_or_init(|| {
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()))
})
.clone()
if let Some(global) = cx.try_global::<GlobalSweepApiKey>() {
return global.0.clone();
}
let entity =
cx.new(|_| ApiKeyState::new(SWEEP_CREDENTIALS_URL, SWEEP_AI_TOKEN_ENV_VAR.clone()));
cx.set_global(GlobalSweepApiKey(entity.clone()));
entity
}
pub fn load_sweep_api_token(cx: &mut App) -> Task<Result<(), language_model::AuthenticateError>> {

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